WordPress Development Recipe: Secure token-based API authentication for Google Analytics v4 REST in custom plugins
Prerequisites and Setup
This recipe assumes a foundational understanding of WordPress plugin development and familiarity with the Google Cloud Platform (GCP) console. We’ll be leveraging Google’s OAuth 2.0 service accounts for secure API access. Ensure you have a Google Cloud project set up and billing enabled, as API calls may incur costs.
The core of this authentication mechanism relies on a JSON Web Token (JWT) generated by a GCP service account. This token is then used to authenticate requests to the Google Analytics Data API v1.
Step 1: Create a GCP Service Account and Download Credentials
Navigate to the GCP Console, go to “IAM & Admin” > “Service Accounts”. Click “Create Service Account”. Give it a descriptive name (e.g., “wordpress-ga4-api-access”) and a unique ID. For the role, grant it the “Google Analytics Viewer” role (or a more granular role if your plugin only requires specific permissions). This is crucial for security; avoid granting excessive privileges.
After creation, click on the service account, go to the “Keys” tab, and click “Add Key” > “Create new key”. Select “JSON” as the key type and click “Create”. This will download a JSON file containing your service account’s private key and client email. Treat this file with extreme care; it’s equivalent to a password. Store it securely on your server, outside of your web-accessible directories.
The downloaded JSON file will look something like this:
{
"type": "service_account",
"project_id": "your-gcp-project-id",
"private_key_id": "your-private-key-id",
"private_key": "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT\n-----END PRIVATE KEY-----\n",
"client_email": "your-service-account-email@your-gcp-project-id.iam.gserviceaccount.com",
"client_id": "your-client-id",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account-email%40your-gcp-project-id.iam.gserviceaccount.com"
}
Step 2: Securely Store Service Account Credentials in WordPress
Never hardcode credentials directly into your plugin files. The most secure method is to store the JSON content in a WordPress option, encrypted. Alternatively, for higher security, store the file outside the webroot and load its contents using PHP’s file system functions, ensuring strict file permissions.
For this recipe, we’ll demonstrate storing the JSON content in a WordPress option. You’ll need to implement encryption/decryption. WordPress’s built-in `wp_salt` and `wp_hash_password` functions can be leveraged, or a more robust library like the `php-encryption` library by defuse. For simplicity here, we’ll show the conceptual storage and retrieval, assuming you’ll add robust encryption.
In your plugin’s settings page or activation hook, you might save the JSON content:
// Assuming $service_account_json_content is the raw JSON string from the downloaded file $option_name = 'my_plugin_ga4_service_account_credentials'; $encrypted_credentials = encrypt_data( $service_account_json_content ); // Implement your encryption function update_option( $option_name, $encrypted_credentials );
And retrieve it when needed:
function get_ga4_service_account_credentials() {
$option_name = 'my_plugin_ga4_service_account_credentials';
$encrypted_credentials = get_option( $option_name );
if ( ! $encrypted_credentials ) {
return false; // Or throw an exception
}
$decrypted_credentials = decrypt_data( $encrypted_credentials ); // Implement your decryption function
return json_decode( $decrypted_credentials, true );
}
Step 3: Implement JWT Generation and Token Acquisition
Google’s service accounts use JWTs signed with their private key to obtain access tokens. We’ll need a PHP library to handle JWT creation and signing. The `firebase/php-jwt` library is a popular and robust choice.
Install it via Composer:
composer require firebase/php-jwt
Now, let’s create a function to generate the JWT and exchange it for an access token:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
/**
* Generates a JWT and exchanges it for a Google OAuth2 access token.
*
* @param array $service_account_credentials Service account credentials array.
* @return string|false The access token on success, false on failure.
*/
function get_google_oauth2_access_token( array $service_account_credentials ) {
$client_email = $service_account_credentials['client_email'];
$private_key = $service_account_credentials['private_key'];
$token_uri = 'https://oauth2.googleapis.com/token'; // Google's token endpoint
// JWT Payload
$payload = [
'iss' => $client_email,
'scope' => 'https://www.googleapis.com/auth/analytics.readonly', // Adjust scope as needed
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => time() + 3600, // Token expires in 1 hour
'iat' => time(),
];
// Generate JWT
$jwt = JWT::encode( $payload, $private_key, 'RS256' );
// Request Access Token
$request_body = [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
];
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $token_uri );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $request_body ) );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); // Crucial for security
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $http_code !== 200 ) {
// Log error: $response contains error details
error_log( "Google OAuth2 Token Error: " . $response );
return false;
}
$token_data = json_decode( $response, true );
if ( isset( $token_data['access_token'] ) ) {
return $token_data['access_token'];
} else {
// Log error: Missing access_token in response
error_log( "Google OAuth2 Token Response Error: Missing access_token. Response: " . $response );
return false;
}
}
Step 4: Make Authenticated Requests to the Google Analytics Data API v1
With a valid access token, you can now query the Google Analytics Data API v1. The API endpoint for the `runReport` method is:
https://analyticsdata.googleapis.com/v1beta/properties/YOUR_PROPERTY_ID:runReport
Replace YOUR_PROPERTY_ID with your actual Google Analytics 4 Property ID.
Here’s an example of how to construct and send a request:
/**
* Fetches a report from the Google Analytics Data API v1.
*
* @param string $property_id Your GA4 Property ID.
* @param array $report_request The API request body for runReport.
* @return array|false The report data on success, false on failure.
*/
function get_ga4_report( string $property_id, array $report_request ) {
$credentials = get_ga4_service_account_credentials();
if ( ! $credentials ) {
error_log( "GA4 API: Failed to retrieve service account credentials." );
return false;
}
$access_token = get_google_oauth2_access_token( $credentials );
if ( ! $access_token ) {
error_log( "GA4 API: Failed to obtain access token." );
return false;
}
$api_url = "https://analyticsdata.googleapis.com/v1beta/properties/{$property_id}:runReport";
$headers = [
'Authorization: Bearer ' . $access_token,
'Content-Type: application/json',
];
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $api_url );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $report_request ) );
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); // Crucial for security
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $http_code !== 200 ) {
error_log( "GA4 API Error (HTTP {$http_code}): " . $response );
return false;
}
return json_decode( $response, true );
}
// Example Usage:
$ga4_property_id = 'YOUR_PROPERTY_ID'; // Replace with your GA4 Property ID
$report_params = [
'dateRanges' => [
[
'startDate' => '2023-01-01',
'endDate' => 'today',
],
],
'dimensions' => [
['name' => 'date'],
['name' => 'country'],
],
'metrics' => [
['name' => 'activeUsers'],
['name' => 'sessions'],
],
'limit' => 1000, // Example limit
];
$report_data = get_ga4_report( $ga4_property_id, $report_params );
if ( $report_data ) {
// Process $report_data
echo '<pre>';
print_r( $report_data );
echo '</pre>';
} else {
echo 'Failed to retrieve GA4 report.';
}
Security Considerations and Best Practices
Credential Security: The service account JSON file is highly sensitive. Store it outside the webroot and restrict file permissions to the absolute minimum required. If storing in WordPress options, ensure robust encryption and decryption are implemented. Regularly rotate service account keys.
Least Privilege: Grant the service account only the necessary permissions (e.g., “Google Analytics Viewer”) in GCP. Avoid using broad roles like “Editor” or “Owner”.
Scope Management: Explicitly define the OAuth scopes required in the JWT payload. The example uses https://www.googleapis.com/auth/analytics.readonly, which is appropriate for read-only access. Adjust if your plugin needs to write data (though the Data API v1 is primarily for reading).
Error Handling and Logging: Implement comprehensive error handling for API requests and token generation. Log errors to a secure location (e.g., WordPress debug log) for troubleshooting. Never expose sensitive error details to the end-user.
Token Expiration and Refresh: The access tokens obtained are short-lived (typically 1 hour). The JWT itself is also time-bound. The `get_google_oauth2_access_token` function handles re-authentication on each call. For high-frequency access, consider caching the access token and its expiration time, but be mindful of security implications and potential staleness.
API Versioning: Always be aware of the API version you are using (e.g., `v1beta`). Google may deprecate older versions. Keep your plugin updated to use the latest stable API versions.
Rate Limiting: Be aware of Google Analytics API rate limits. Implement backoff strategies or caching if your plugin makes a high volume of requests.