How to securely integrate Salesforce CRM endpoints into WordPress custom plugins using Shortcode API
Establishing Secure Salesforce API Connections in WordPress
Integrating Salesforce CRM data into a WordPress site via custom plugins offers immense value, enabling dynamic content display and streamlined workflows. However, the security of these integrations is paramount. This guide details a robust approach to securely connecting to Salesforce API endpoints using WordPress’s Shortcode API, focusing on OAuth 2.0 for authentication and best practices for credential management.
Prerequisites and Setup
Before diving into the code, ensure you have the following:
- A Salesforce Developer Edition account or a sandbox environment.
- A connected app configured in Salesforce to enable OAuth 2.0. Note down the Consumer Key (Client ID) and Consumer Secret (Client Secret).
- A WordPress installation with administrative access.
- Basic understanding of PHP and WordPress plugin development.
Salesforce Connected App Configuration
In Salesforce, navigate to Setup > Apps > App Manager. Create a new Connected App. Key configurations include:
- Enable OAuth Settings: Check this box.
- Callback URL: This is crucial. For local development, you might use something like
http://localhost/wp-admin/admin-ajax.php?action=salesforce_oauth_callback. For production, use your WordPress site’s URL followed by the same action. - Selected OAuth Scopes: Choose scopes appropriate for your integration, e.g.,
api,refresh_token, offline_access.
Once saved, you will see the Consumer Key and Consumer Secret. Treat the Consumer Secret with the same confidentiality as a password.
WordPress Plugin Structure and Credential Storage
We’ll create a simple WordPress plugin. For storing sensitive credentials like the Consumer Secret and the Salesforce access/refresh tokens, avoid hardcoding them directly in the plugin files. Instead, leverage WordPress’s options API and consider using environment variables or a secure configuration file outside the webroot for production environments. For this example, we’ll use the options API for simplicity, but remember to sanitize and validate all inputs.
Plugin Initialization and Settings Page
The plugin will need a way to store and retrieve the Salesforce API credentials. A simple settings page is a good starting point.
salesforce-integration.php (Main Plugin File)
<?php
/**
* Plugin Name: Salesforce Integration
* Description: Securely integrates Salesforce CRM endpoints into WordPress.
* Version: 1.0
* Author: Your Name
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'SALESFORCE_INTEGRATION_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'SALESFORCE_INTEGRATION_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
// Include necessary files
require_once SALESFORCE_INTEGRATION_PLUGIN_PATH . 'includes/class-salesforce-oauth.php';
require_once SALESFORCE_INTEGRATION_PLUGIN_PATH . 'includes/class-salesforce-shortcode.php';
require_once SALESFORCE_INTEGRATION_PLUGIN_PATH . 'admin/class-salesforce-settings.php';
// Initialize classes
function initialize_salesforce_integration() {
new Salesforce_Settings();
new Salesforce_OAuth();
new Salesforce_Shortcode();
}
add_action( 'plugins_loaded', 'initialize_salesforce_integration' );
// Activation hook to set default options
register_activation_hook( __FILE__, 'salesforce_integration_activate' );
function salesforce_integration_activate() {
if ( false === get_option( 'sf_integration_consumer_key' ) ) {
add_option( 'sf_integration_consumer_key', '' );
}
if ( false === get_option( 'sf_integration_consumer_secret' ) ) {
add_option( 'sf_integration_consumer_secret', '' );
}
if ( false === get_option( 'sf_integration_instance_url' ) ) {
add_option( 'sf_integration_instance_url', '' ); // e.g., https://yourdomain.my.salesforce.com
}
if ( false === get_option( 'sf_integration_redirect_uri' ) ) {
add_option( 'sf_integration_redirect_uri', admin_url( 'admin-ajax.php?action=salesforce_oauth_callback' ) );
}
}
?>
admin/class-salesforce-settings.php
<div class="wrap">
<h1>Salesforce Integration Settings</h1>
<form method="post" action="options.php">
</form>
<p><a href="" class="button button-primary">Connect to Salesforce</a></p>
<p>After connecting, you may need to refresh your access token periodically. The system attempts to do this automatically.</p>
</div>
OAuth 2.0 Authorization Code Flow Implementation
The OAuth 2.0 Authorization Code flow is the recommended method for server-side web applications like WordPress plugins. It involves redirecting the user to Salesforce to grant permission, then exchanging an authorization code for an access token.
includes/class-salesforce-oauth.php
load_settings();
add_action( 'wp_ajax_salesforce_oauth_init', array( $this, 'initiate_oauth' ) );
add_action( 'wp_ajax_salesforce_oauth_callback', array( $this, 'handle_oauth_callback' ) );
add_action( 'admin_init', array( $this, 'maybe_refresh_token' ) );
}
private function load_settings() {
$options = get_option( 'sf_integration_settings' );
$this->consumer_key = isset( $options['consumer_key'] ) ? $options['consumer_key'] : '';
$this->consumer_secret = isset( $options['consumer_secret'] ) ? $options['consumer_secret'] : '';
$this->instance_url = isset( $options['instance_url'] ) ? rtrim( $options['instance_url'], '/' ) : '';
$this->redirect_uri = isset( $options['redirect_uri'] ) ? $options['redirect_uri'] : '';
if ( empty( $this->consumer_key ) || empty( $this->consumer_secret ) || empty( $this->instance_url ) ) {
// Optionally display an admin notice if settings are incomplete
add_action( 'admin_notices', array( $this, 'admin_notice_missing_settings' ) );
}
}
public function admin_notice_missing_settings() {
?>
<div class="notice notice-error is-dismissible">
<p><?php _e( 'Salesforce Integration: Please configure your Connected App credentials in the Salesforce Integration settings page.', 'salesforce-integration' ); ?></p>
</div>
consumer_key ) || empty( $this->consumer_secret ) || empty( $this->instance_url ) || empty( $this->redirect_uri ) ) {
wp_die( 'Salesforce API credentials are not fully configured. Please check your settings.' );
}
$auth_url = $this->instance_url . '/services/oauth2/authorize';
$params = array(
'response_type' => 'code',
'client_id' => $this->consumer_key,
'redirect_uri' => $this->redirect_uri,
'scope' => 'api refresh_token offline_access', // Adjust scopes as needed
);
$auth_url = add_query_arg( $params, $auth_url );
wp_redirect( $auth_url );
exit;
}
public function handle_oauth_callback() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to perform this action.' );
}
if ( isset( $_GET['error'] ) ) {
// Handle errors from Salesforce
$error_message = sanitize_text_field( $_GET['error_description'] ?? $_GET['error'] );
wp_die( 'Salesforce OAuth Error: ' . $error_message );
}
if ( ! isset( $_GET['code'] ) ) {
wp_die( 'Invalid OAuth callback. Missing authorization code.' );
}
$auth_code = sanitize_text_field( $_GET['code'] );
$token_url = $this->instance_url . '/services/oauth2/token';
$body = array(
'grant_type' => 'authorization_code',
'code' => $auth_code,
'client_id' => $this->consumer_key,
'client_secret' => $this->consumer_secret,
'redirect_uri' => $this->redirect_uri,
);
$response = wp_remote_post( $token_url, array(
'method' => 'POST',
'body' => $body,
'headers' => array( 'Content-Type' => 'application/x-www-form-urlencoded' ),
'timeout' => 60,
) );
if ( is_wp_error( $response ) ) {
wp_die( 'Error obtaining access token: ' . $response->get_error_message() );
}
$body = wp_remote_retrieve_body( $response );
$token_data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE || ! isset( $token_data['access_token'] ) ) {
wp_die( 'Failed to parse token response or token not found. Response: ' . esc_html( $body ) );
}
// Store tokens securely
update_option( $this->auth_token_option_name, $token_data['access_token'] );
update_option( $this->refresh_token_option_name, $token_data['refresh_token'] );
update_option( $this->token_expiry_option_name, time() + intval( $token_data['expires_in'] ) );
// Redirect back to settings page or a success page
wp_redirect( admin_url( 'options-general.php?page=salesforce-integration&message=oauth_success' ) );
exit;
}
public function get_access_token() {
$access_token = get_option( $this->auth_token_option_name );
$refresh_token = get_option( $this->refresh_token_option_name );
$expiry_time = get_option( $this->token_expiry_option_name );
// Check if token is expired or missing
if ( empty( $access_token ) || empty( $refresh_token ) || ( $expiry_time && time() >= $expiry_time ) ) {
if ( ! empty( $refresh_token ) ) {
return $this->refresh_access_token( $refresh_token );
} else {
// No refresh token available, prompt user to re-authenticate
return false;
}
}
return $access_token;
}
private function refresh_access_token( $refresh_token ) {
if ( empty( $this->consumer_key ) || empty( $this->consumer_secret ) || empty( $this->instance_url ) ) {
return false; // Cannot refresh without credentials
}
$token_url = $this->instance_url . '/services/oauth2/token';
$body = array(
'grant_type' => 'refresh_token',
'refresh_token' => $refresh_token,
'client_id' => $this->consumer_key,
'client_secret' => $this->consumer_secret,
);
$response = wp_remote_post( $token_url, array(
'method' => 'POST',
'body' => $body,
'headers' => array( 'Content-Type' => 'application/x-www-form-urlencoded' ),
'timeout' => 60,
) );
if ( is_wp_error( $response ) ) {
error_log( 'Salesforce Token Refresh Error: ' . $response->get_error_message() );
return false;
}
$body = wp_remote_retrieve_body( $response );
$token_data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE || ! isset( $token_data['access_token'] ) ) {
error_log( 'Salesforce Token Refresh Failed. Response: ' . esc_html( $body ) );
// If refresh token is invalid, clear stored tokens
if ( isset( $token_data['error'] ) && $token_data['error'] === 'invalid_grant' ) {
delete_option( $this->auth_token_option_name );
delete_option( $this->refresh_token_option_name );
delete_option( $this->token_expiry_option_name );
}
return false;
}
// Update stored tokens
update_option( $this->auth_token_option_name, $token_data['access_token'] );
update_option( $this->refresh_token_option_name, $token_data['refresh_token'] ?? $refresh_token ); // Keep old refresh token if new one not provided
update_option( $this->token_expiry_option_name, time() + intval( $token_data['expires_in'] ) );
return $token_data['access_token'];
}
// This function is called on admin_init to proactively refresh tokens if needed.
public function maybe_refresh_token() {
if ( is_admin() && current_user_can( 'manage_options' ) ) {
$expiry_time = get_option( $this->token_expiry_option_name );
// Refresh if token expires within the next hour (3600 seconds)
if ( $expiry_time && ( time() + 3600 ) >= $expiry_time ) {
$refresh_token = get_option( $this->refresh_token_option_name );
if ( $refresh_token ) {
$this->refresh_access_token( $refresh_token );
}
}
}
}
public function get_salesforce_api_url() {
return $this->instance_url . '/services/data/';
}
}
?>
Creating a Shortcode to Fetch Salesforce Data
Now, let's create a shortcode that users can place in their WordPress posts or pages to display Salesforce data. This shortcode will use the authenticated Salesforce API client.
includes/class-salesforce-shortcode.php
salesforce_oauth = new Salesforce_OAuth(); // Instantiate OAuth class to access its methods
add_shortcode( 'salesforce_account_list', array( $this, 'render_account_list_shortcode' ) );
add_shortcode( 'salesforce_contact_search', array( $this, 'render_contact_search_shortcode' ) );
}
public function render_account_list_shortcode( $atts ) {
// Shortcode attributes (e.g., [salesforce_account_list limit="10"])
$atts = shortcode_atts( array(
'limit' => 10,
), $atts, 'salesforce_account_list' );
$limit = intval( $atts['limit'] );
$access_token = $this->salesforce_oauth->get_access_token();
if ( ! $access_token ) {
return '<p>Error: Could not authenticate with Salesforce. Please connect your account.</p>';
}
$instance_url = $this->salesforce_oauth->get_salesforce_api_url();
$api_endpoint = $instance_url . 'v58.0/query/?q=' . urlencode( "SELECT Id, Name, Industry, Phone FROM Account ORDER BY Name LIMIT {$limit}" ); // Use a specific API version
$response = wp_remote_get( $api_endpoint, array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
),
'timeout' => 30,
) );
if ( is_wp_error( $response ) ) {
return '<p>Error fetching accounts: ' . esc_html( $response->get_error_message() ) . '</p>';
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE || ! isset( $data['records'] ) ) {
return '<p>Error processing Salesforce response. Response: ' . esc_html( $body ) . '</p>';
}
// Start output buffering
ob_start();
?>
<div class="salesforce-accounts">
<h3>Salesforce Accounts</h3>
<ul>
<li>
<strong><?php echo esc_html( $account['Name'] ); ?></strong>
(Industry: <?php echo esc_html( $account['Industry'] ?? 'N/A' ); ?>, Phone: <?php echo esc_html( $account['Phone'] ?? 'N/A' ); ?>)
</li>
</ul>
</div>
<div class="salesforce-contact-search">
<h3>Search Salesforce Contacts</h3>
<form method="post" action="">
<input type="text" name="sf_contact_search_name" placeholder="Enter Contact Name" />
<input type="submit" name="sf_contact_search_submit" value="Search" class="button" />
<?php wp_nonce_field( 'sf_contact_search_nonce_action', 'sf_contact_search_nonce_field' ); ?>
</form>
<div id="sf-contact-results">
display_contact_search_results( $search_name );
} else {
echo '<p>Please enter a name to search.</p>';
}
}
?>
</div>
</div>
salesforce_oauth->get_access_token();
if ( ! $access_token ) {
echo '<p>Error: Could not authenticate with Salesforce.</p>';
return;
}
$instance_url = $this->salesforce_oauth->get_salesforce_api_url();
// Using SOSL (Salesforce Object Search Language) for flexible searching
$search_query = urlencode( "FIND {$name} IN ALL FIELDS RETURNING Contact(Id, Name, Email, Phone, Account.Name)" );
$api_endpoint = $instance_url . 'v58.0/search/?q=' . $search_query;
$response = wp_remote_get( $api_endpoint, array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
),
'timeout' => 30,
) );
if ( is_wp_error( $response ) ) {
echo '<p>Error fetching contacts: ' . esc_html( $response->get_error_message() ) . '</p>';
return;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE || ! isset( $data['searchRecords'] ) ) {
echo '<p>Error processing Salesforce response. Response: ' . esc_html( $body ) . '</p>';
return;
}
if ( empty( $data['searchRecords'] ) ) {
echo '<p>No contacts found matching your search criteria.</p>';
return;
}
?>
<h4>Search Results</h4>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Account</th>
</tr>
</thead>
<tbody>
<tr>
<td><?php echo esc_html( $contact['Name'] ); ?></td>
<td><?php echo esc_html( $contact['Email'] ?? 'N/A' ); ?></td>
<td><?php echo esc_html( $contact['Phone'] ?? 'N/A' ); ?></td>
<td><?php echo esc_html( $contact['Account']['Name'] ?? 'N/A' ); ?></td>
</tr>
</tbody>
</table>
Usage in WordPress
Once the plugin is activated and the Salesforce credentials are set up via the WordPress admin menu (Options > Salesforce Integration), you can use the shortcodes:
- To display a list of Salesforce Accounts:
[salesforce_account_list limit="5"] - To display the contact search form:
[salesforce_contact_search]
When a user first encounters a shortcode that requires authentication, they will be prompted to click the "Connect to Salesforce" button on the settings page. After successful OAuth, the shortcodes will function as expected.
Security Considerations and Best Practices
- Credential Storage: For production, avoid storing secrets directly in the WordPress database options table. Use environment variables (e.g., via a
wp-config.phpmodification or a plugin that loads them) or a secure configuration file outside the web root. - HTTPS: Ensure your WordPress site and Salesforce are accessed over HTTPS to protect data in transit.
- OAuth Scopes: Grant only the necessary OAuth scopes to your Connected App. Avoid overly broad permissions.
- API Versioning: Pin your API calls to a specific Salesforce API version (e.g.,
v58.0) to prevent unexpected changes when Salesforce updates its API. - Error Handling: Implement robust error handling for API requests, including network issues, authentication failures, and invalid responses. Log errors for debugging.
- Rate Limiting: Be mindful of Salesforce API rate limits. Implement caching and efficient querying to avoid hitting limits.
- Input Sanitization: Always sanitize user inputs before using them in API queries or displaying them. Use WordPress functions like
sanitize_text_field,esc_html, andesc_url. - Nonce Verification: For any form submissions handled via AJAX or POST requests, always use nonces to prevent CSRF attacks.
- Token Management: Regularly review and rotate Salesforce access and refresh tokens. Implement automated refresh mechanisms as shown.