WordPress Development Recipe: Secure token-based API authentication for Salesforce CRM in custom plugins
Prerequisites and Setup
This recipe assumes a foundational understanding of WordPress plugin development, PHP, and Salesforce CRM’s API capabilities. You will need:
- A WordPress installation with administrative access.
- A Salesforce Developer Edition account or a sandbox environment.
- A Salesforce Connected App configured for API access (OAuth 2.0).
- Basic familiarity with RESTful API concepts.
The Salesforce Connected App is crucial. It acts as the intermediary, allowing your WordPress plugin to securely authenticate with Salesforce. Ensure you have generated a Consumer Key and Consumer Secret for this app. For this recipe, we’ll focus on the “Username-Password Flow” for simplicity in a controlled plugin environment, though OAuth 2.0 flows like “JWT Bearer Flow” are generally more secure for server-to-server integrations.
Salesforce Connected App Configuration
Navigate to your Salesforce Setup. Under “Apps” > “App Manager”, create a new “Connected App”.
- Basic Information: Provide a descriptive name (e.g., “WordPress CRM Integration”).
- API (Enable OAuth Settings): Check this box.
- Callback URL: For Username-Password flow, this is less critical but often required. A placeholder like
https://localhost/callbackis acceptable if not directly used. - Selected OAuth Scopes: Crucially, select scopes that grant the necessary API access. For example, “Access and manage your data (api)” and “Perform requests on your behalf at any time (refresh_token, offline_access)”.
After saving, you will be presented with the Consumer Key and Consumer Secret. Treat the Consumer Secret as a password; do not expose it client-side.
WordPress Plugin Structure and Initialization
We’ll create a simple WordPress plugin. The core logic will reside in a PHP class. For security, sensitive credentials (Consumer Key, Consumer Secret, Salesforce Username, Password, Security Token) should ideally be stored in wp-config.php or a secure, non-public location, not directly in the plugin’s code or the database.
Create a directory for your plugin, e.g., wp-content/plugins/salesforce-api-auth/. Inside, create a main plugin file, e.g., salesforce-api-auth.php.
salesforce-api-auth.php – Main Plugin File
This file contains the plugin header and the main class instantiation.
/*
Plugin Name: Salesforce API Authentication
Plugin URI: https://example.com/
Description: Secure token-based API authentication for Salesforce CRM.
Version: 1.0
Author: Your Name
Author URI: https://example.com/
License: GPL2
*/
// Prevent direct access to the file
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define constants for credentials (ideally loaded from wp-config.php)
// For demonstration, hardcoded here. In production, use:
// define( 'SALESFORCE_CONSUMER_KEY', getenv('SALESFORCE_CONSUMER_KEY') ?: 'YOUR_CONSUMER_KEY' );
// define( 'SALESFORCE_CONSUMER_SECRET', getenv('SALESFORCE_CONSUMER_SECRET') ?: 'YOUR_CONSUMER_SECRET' );
// define( 'SALESFORCE_USERNAME', getenv('SALESFORCE_USERNAME') ?: 'YOUR_SALESFORCE_USERNAME' );
// define( 'SALESFORCE_PASSWORD', getenv('SALESFORCE_PASSWORD') ?: 'YOUR_SALESFORCE_PASSWORD' );
// define( 'SALESFORCE_SECURITY_TOKEN', getenv('SALESFORCE_SECURITY_TOKEN') ?: 'YOUR_SECURITY_TOKEN' );
// Example using wp-config.php (recommended)
if ( ! defined( 'SALESFORCE_CONSUMER_KEY' ) ) {
define( 'SALESFORCE_CONSUMER_KEY', 'YOUR_CONSUMER_KEY_FROM_WP_CONFIG' );
}
if ( ! defined( 'SALESFORCE_CONSUMER_SECRET' ) ) {
define( 'SALESFORCE_CONSUMER_SECRET', 'YOUR_CONSUMER_SECRET_FROM_WP_CONFIG' );
}
if ( ! defined( 'SALESFORCE_USERNAME' ) ) {
define( 'SALESFORCE_USERNAME', 'YOUR_SALESFORCE_USERNAME_FROM_WP_CONFIG' );
}
if ( ! defined( 'SALESFORCE_PASSWORD' ) ) {
define( 'SALESFORCE_PASSWORD', 'YOUR_SALESFORCE_PASSWORD_FROM_WP_CONFIG' );
}
if ( ! defined( 'SALESFORCE_SECURITY_TOKEN' ) ) {
define( 'SALESFORCE_SECURITY_TOKEN', 'YOUR_SECURITY_TOKEN_FROM_WP_CONFIG' );
}
// Include the main class file
require_once plugin_dir_path( __FILE__ ) . 'includes/class-salesforce-api-auth.php';
// Instantiate the plugin class
if ( class_exists( 'Salesforce_API_Auth' ) ) {
$salesforce_auth = new Salesforce_API_Auth();
$salesforce_auth->init();
}
Salesforce API Authentication Class
Create a new directory includes/ within your plugin directory. Inside includes/, create class-salesforce-api-auth.php.
includes/class-salesforce-api-auth.php
This class will handle the token acquisition and management.
<?php
/**
* Salesforce API Authentication Class
*/
class Salesforce_API_Auth {
private $consumer_key;
private $consumer_secret;
private $username;
private $password;
private $security_token;
private $access_token;
private $instance_url;
private $token_expiry;
private $token_storage_key = 'sf_access_token_data'; // Key for transient storage
public function __construct() {
$this->consumer_key = defined( 'SALESFORCE_CONSUMER_KEY' ) ? SALESFORCE_CONSUMER_KEY : '';
$this->consumer_secret = defined( 'SALESFORCE_CONSUMER_SECRET' ) ? SALESFORCE_CONSUMER_SECRET : '';
$this->username = defined( 'SALESFORCE_USERNAME' ) ? SALESFORCE_USERNAME : '';
$this->password = defined( 'SALESFORCE_PASSWORD' ) ? SALESFORCE_PASSWORD : '';
$this->security_token = defined( 'SALESFORCE_SECURITY_TOKEN' ) ? SALESFORCE_SECURITY_TOKEN : '';
}
/**
* Initialize hooks and load token data.
*/
public function init() {
// Load existing token data if available
$this->load_token_data();
// Optionally, hook into a WordPress action to refresh token if expired
// For example, on admin_init or a specific plugin action.
// add_action( 'admin_init', array( $this, 'ensure_valid_token' ) );
}
/**
* Get Salesforce login URL for Username-Password flow.
*
* @return string The Salesforce login URL.
*/
private function get_login_url() {
// Use the production URL or the sandbox URL based on your Salesforce instance.
// For sandbox: https://test.salesforce.com/services/oauth2/token
return 'https://login.salesforce.com/services/oauth2/token';
}
/**
* Request an access token from Salesforce.
*
* @return bool True on success, false on failure.
*/
public function request_access_token() {
if ( empty( $this->consumer_key ) || empty( $this->consumer_secret ) || empty( $this->username ) || empty( $this->password ) || empty( $this->security_token ) ) {
error_log( 'Salesforce API Auth: Missing credentials.' );
return false;
}
$url = $this->get_login_url();
$body = array(
'grant_type' => 'password',
'client_id' => $this->consumer_key,
'client_secret' => $this->consumer_secret,
'username' => $this->username,
'password' => $this->password . $this->security_token,
);
$response = wp_remote_post( $url, array(
'method' => 'POST',
'body' => $body,
'timeout' => 30,
'sslverify' => true, // Set to false for local testing if SSL issues arise, but NEVER in production.
) );
if ( is_wp_error( $response ) ) {
error_log( 'Salesforce API Auth: wp_remote_post error: ' . $response->get_error_message() );
return false;
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
$data = json_decode( $response_body, true );
if ( $response_code === 200 && isset( $data['access_token'] ) && isset( $data['instance_url'] ) ) {
$this->access_token = $data['access_token'];
$this->instance_url = $data['instance_url'];
// Salesforce token expiry is typically 2 hours (7200 seconds)
$this->token_expiry = time() + ( isset( $data['expires_in'] ) ? $data['expires_in'] : 7200 );
$this->save_token_data();
return true;
} else {
error_log( 'Salesforce API Auth: Failed to get access token. Response: ' . print_r( $data, true ) );
return false;
}
}
/**
* Load token data from WordPress transients.
*/
private function load_token_data() {
$token_data = get_transient( $this->token_storage_key );
if ( $token_data && is_array( $token_data ) ) {
$this->access_token = isset( $token_data['access_token'] ) ? $token_data['access_token'] : null;
$this->instance_url = isset( $token_data['instance_url'] ) ? $token_data['instance_url'] : null;
$this->token_expiry = isset( $token_data['token_expiry'] ) ? $token_data['token_expiry'] : 0;
}
}
/**
* Save token data to WordPress transients.
* Transients expire after 4 hours (14400 seconds) to ensure we re-check before Salesforce's 2-hour expiry.
*/
private function save_token_data() {
if ( ! empty( $this->access_token ) && ! empty( $this->instance_url ) ) {
$data_to_save = array(
'access_token' => $this->access_token,
'instance_url' => $this->instance_url,
'token_expiry' => $this->token_expiry,
);
// Set transient to expire slightly before Salesforce's actual expiry to allow for refresh attempts.
// Salesforce tokens are typically 2 hours (7200s). We'll set transient to expire in 4 hours (14400s).
set_transient( $this->token_storage_key, $data_to_save, 14400 );
}
}
/**
* Check if the current token is expired and attempt to refresh if necessary.
*
* @return bool True if a valid token is available, false otherwise.
*/
public function ensure_valid_token() {
if ( ! empty( $this->access_token ) && $this->token_expiry > time() ) {
// Token is still valid
return true;
}
// Token is expired or not loaded, attempt to get a new one
if ( $this->request_access_token() ) {
return true;
}
// Failed to get a new token
$this->access_token = null;
$this->instance_url = null;
$this->token_expiry = 0;
delete_transient( $this->token_storage_key ); // Clear potentially stale data
return false;
}
/**
* Get the current access token.
*
* @return string|null The access token or null if not available.
*/
public function get_access_token() {
if ( $this->ensure_valid_token() ) {
return $this->access_token;
}
return null;
}
/**
* Get the Salesforce instance URL.
*
* @return string|null The instance URL or null if not available.
*/
public function get_instance_url() {
if ( $this->ensure_valid_token() ) {
return $this->instance_url;
}
return null;
}
/**
* Make a generic API request to Salesforce.
*
* @param string $method The HTTP method (GET, POST, PUT, DELETE).
* @param string $endpoint The Salesforce API endpoint (e.g., '/services/data/v58.0/sobjects/Account/').
* @param array $body The request body for POST/PUT requests.
* @param array $headers Additional headers.
* @return array|WP_Error The API response or a WP_Error object on failure.
*/
public function make_api_request( $method, $endpoint, $body = array(), $headers = array() ) {
if ( ! $this->ensure_valid_token() ) {
return new WP_Error( 'salesforce_auth_error', 'Could not obtain a valid Salesforce access token.' );
}
$url = $this->instance_url . $endpoint;
$default_headers = array(
'Authorization' => 'Bearer ' . $this->access_token,
'Content-Type' => 'application/json',
);
$request_headers = array_merge( $default_headers, $headers );
$args = array(
'method' => strtoupper( $method ),
'headers' => $request_headers,
'timeout' => 30,
'sslverify' => true,
);
if ( ! empty( $body ) && ( strtoupper( $method ) === 'POST' || strtoupper( $method ) === 'PUT' ) ) {
// Ensure body is JSON encoded if Content-Type is application/json
if ( ! isset( $request_headers['Content-Type'] ) || $request_headers['Content-Type'] === 'application/json' ) {
$args['body'] = json_encode( $body );
} else {
$args['body'] = $body; // Assume it's already formatted correctly
}
}
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
error_log( 'Salesforce API Auth: API request error: ' . $response->get_error_message() );
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
$decoded_body = json_decode( $response_body, true );
// Handle potential token expiry during API call
if ( $response_code === 401 && isset( $decoded_body['error'] ) && $decoded_body['error'] === 'invalid_grant' ) {
// Token might have expired, try to refresh and retry once
delete_transient( $this->token_storage_key ); // Clear stale token
$this->access_token = null; // Force re-authentication
$this->token_expiry = 0;
if ( $this->request_access_token() ) {
// Retry the request with the new token
return $this->make_api_request( $method, $endpoint, $body, $headers );
} else {
return new WP_Error( 'salesforce_auth_error', 'Failed to refresh Salesforce token after 401 error.' );
}
}
if ( $response_code >= 200 && $response_code < 300 ) {
return $decoded_body;
} else {
error_log( 'Salesforce API Auth: API request failed. Code: ' . $response_code . ', Response: ' . print_r( $decoded_body, true ) );
return new WP_Error( 'salesforce_api_error', 'Salesforce API request failed.', array( 'status' => $response_code, 'response' => $decoded_body ) );
}
}
/**
* Example: Get a list of Accounts.
*
* @return array|WP_Error
*/
public function get_accounts() {
// Using API version 58.0 as an example. Adjust as needed.
$endpoint = '/services/data/v58.0/sobjects/Account/';
return $this->make_api_request( 'GET', $endpoint );
}
/**
* Example: Create a new Lead.
*
* @param array $lead_data Data for the new lead.
* @return array|WP_Error
*/
public function create_lead( $lead_data ) {
$endpoint = '/services/data/v58.0/sobjects/Lead/';
return $this->make_api_request( 'POST', $endpoint, $lead_data );
}
}
Implementing the Credentials in wp-config.php
To avoid hardcoding sensitive credentials, it’s best practice to define them as constants in your wp-config.php file. This file is located in the root directory of your WordPress installation.
// Add these lines to your wp-config.php file, above the /* That's all, stop editing! Happy publishing. */ line. // Salesforce API Credentials define( 'SALESFORCE_CONSUMER_KEY', 'YOUR_SALESFORCE_CONSUMER_KEY' ); define( 'SALESFORCE_CONSUMER_SECRET', 'YOUR_SALESFORCE_CONSUMER_SECRET' ); define( 'SALESFORCE_USERNAME', 'YOUR_SALESFORCE_USERNAME' ); define( 'SALESFORCE_PASSWORD', 'YOUR_SALESFORCE_PASSWORD' ); define( 'SALESFORCE_SECURITY_TOKEN', 'YOUR_SALESFORCE_SECURITY_TOKEN' ); /* That's all, stop editing! Happy publishing. */
Important Security Note: If your Salesforce username or password contains special characters that might conflict with PHP syntax (e.g., quotes), ensure they are properly escaped or use alternative methods like environment variables if your hosting supports it. For maximum security, consider using a secrets management system and fetching these values dynamically.
Usage Example: Triggering an API Call
You can now use the Salesforce_API_Auth class to interact with Salesforce. Here’s an example of how you might trigger an API call, for instance, when a WordPress post is published.
Hooking into WordPress Actions
Add the following code to your main plugin file (salesforce-api-auth.php) or a separate file included by it.
// Add this to your main plugin file (salesforce-api-auth.php)
// ... (previous code for class definition and instantiation) ...
// Hook into post save action
add_action( 'save_post', 'sf_api_sync_post_to_salesforce', 10, 3 );
function sf_api_sync_post_to_salesforce( $post_id, $post, $update ) {
// Only proceed if this is not an autosave and if the user has permissions
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return $post_id;
}
// Check if the post type is one you want to sync (e.g., 'post')
if ( 'post' !== $post->post_type ) {
return $post_id;
}
// Ensure the global $salesforce_auth object is available
global $salesforce_auth;
if ( ! isset( $salesforce_auth ) || ! is_a( $salesforce_auth, 'Salesforce_API_Auth' ) ) {
// Instantiate if not already done (e.g., if init() wasn't called early enough)
$salesforce_auth = new Salesforce_API_Auth();
$salesforce_auth->init(); // Ensure token is loaded/refreshed
}
// Attempt to get a valid token
if ( ! $salesforce_auth->ensure_valid_token() ) {
error_log( 'SF API Sync: Failed to get valid Salesforce token for post ID ' . $post_id );
return $post_id;
}
// Prepare data for Salesforce Lead object (example)
// You would map post fields to Salesforce Lead fields.
$lead_data = array(
'FirstName' => 'WP User', // Example: Get from post author or a meta field
'LastName' => $post->post_title,
'Company' => 'WordPress Site', // Example
'Email' => '[email protected]', // Example: Get from post author or a meta field
'Status' => 'New',
'Description' => wp_strip_all_tags( $post->post_content ), // Truncate if necessary
);
// Make the API call to create a Lead
$result = $salesforce_auth->create_lead( $lead_data );
if ( is_wp_error( $result ) ) {
error_log( 'SF API Sync: Error creating Salesforce Lead for post ID ' . $post_id . ': ' . $result->get_error_message() );
} else {
// Success! Log the Salesforce Lead ID
$lead_id = isset( $result['id'] ) ? $result['id'] : 'N/A';
error_log( 'SF API Sync: Successfully created Salesforce Lead for post ID ' . $post_id . '. Lead ID: ' . $lead_id );
// Optionally, save the Salesforce Lead ID as post meta
update_post_meta( $post_id, '_salesforce_lead_id', $lead_id );
}
return $post_id;
}
// Example of retrieving data (e.g., in an admin page or AJAX handler)
function sf_api_get_salesforce_accounts() {
global $salesforce_auth;
if ( ! isset( $salesforce_auth ) || ! is_a( $salesforce_auth, 'Salesforce_API_Auth' ) ) {
$salesforce_auth = new Salesforce_API_Auth();
$salesforce_auth->init();
}
if ( ! $salesforce_auth->ensure_valid_token() ) {
return new WP_Error( 'sf_api_error', 'Could not connect to Salesforce.' );
}
$accounts = $salesforce_auth->get_accounts();
if ( is_wp_error( $accounts ) ) {
return $accounts;
}
// Process accounts data
// For example, return a JSON response for an AJAX call
// wp_send_json_success( $accounts );
return $accounts;
}
Error Handling and Logging
Robust error handling is critical for production systems. The provided class uses error_log() for basic logging. For a more sophisticated solution, consider integrating with a dedicated WordPress logging plugin or a centralized logging system (e.g., ELK stack, Splunk).
Key areas for error monitoring include:
- Failed token acquisition (invalid credentials, network issues).
- API request failures (e.g., 400 Bad Request, 403 Forbidden, 500 Internal Server Error from Salesforce).
- Token expiry and refresh failures.
Security Considerations and Best Practices
Credential Management: Never hardcode credentials directly in plugin files that are committed to version control. Use wp-config.php, environment variables, or a secure secrets manager.
OAuth Flows: While Username-Password flow is demonstrated for simplicity, it’s less secure as it requires storing the user’s password and security token. For server-to-server integrations, consider the OAuth 2.0 JWT Bearer Flow, which uses digital certificates for authentication and avoids storing user credentials directly.
API Versioning: Salesforce frequently updates its API. Ensure your plugin specifies and is tested against a compatible API version. The example uses v58.0; update this as needed.
Rate Limiting: Be aware of Salesforce API rate limits. Implement retry mechanisms with exponential backoff for transient errors, but avoid excessive calls that could lead to your integration being throttled or blocked.
Data Validation and Sanitization: Always validate and sanitize data before sending it to Salesforce to prevent data corruption and potential security vulnerabilities.
HTTPS: Ensure all communication with Salesforce uses HTTPS. The wp_remote_post and wp_remote_request functions in WordPress handle this by default when sslverify is true.
Further Enhancements
- Advanced OAuth Flows: Implement JWT Bearer Flow for enhanced security.
- Web-to-Lead/Web-to-Case: For simpler, less secure integrations where direct API access isn’t strictly necessary.
- Salesforce Object Management: Extend the class to handle more Salesforce objects (Contacts, Opportunities, Custom Objects).
- Data Synchronization Logic: Implement more sophisticated two-way synchronization, conflict resolution, and delta updates.
- AJAX Handlers: Create WordPress AJAX actions to trigger Salesforce operations from the front-end or back-end securely.
- Admin Interface: Build a settings page in the WordPress admin to manage Salesforce connection details and view sync logs.