feat: Switch to OAuth 2.0 authentication for Google Merchant API

- Replace service account authentication with OAuth 2.0 user flow
- Add "Authorize with Google" button in admin settings
- Handle OAuth callback and token exchange
- Store refresh token for automatic access token renewal
- Add revoke authorization functionality
- Update admin UI to show authorization status
- Update price updater to use new OAuth credentials
- Add CSRF protection with state parameter

This change supports organizations that have disabled service account
key creation via iam.disableServiceAccountKeyCreation policy.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 08:52:57 +01:00
parent d1f3607895
commit e313fce197
4 changed files with 456 additions and 140 deletions

View File

@@ -44,9 +44,13 @@ class Informatiq_SP_Admin {
// Register settings.
add_action( 'admin_init', array( $this, 'register_settings' ) );
// Handle OAuth callback.
add_action( 'admin_init', array( $this, 'handle_oauth_callback' ) );
// Handle AJAX requests.
add_action( 'wp_ajax_informatiq_sp_manual_sync', array( $this, 'handle_manual_sync' ) );
add_action( 'wp_ajax_informatiq_sp_test_connection', array( $this, 'handle_test_connection' ) );
add_action( 'wp_ajax_informatiq_sp_revoke_auth', array( $this, 'handle_revoke_auth' ) );
// Enqueue admin assets.
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
@@ -72,7 +76,8 @@ class Informatiq_SP_Admin {
public function register_settings() {
// Register settings.
register_setting( 'informatiq_sp_settings', 'informatiq_sp_merchant_id' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_service_account' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_client_id' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_client_secret' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_minimum_margin' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_auto_update_enabled' );
register_setting( 'informatiq_sp_settings', 'informatiq_sp_update_frequency' );
@@ -109,9 +114,17 @@ class Informatiq_SP_Admin {
);
add_settings_field(
'informatiq_sp_service_account',
__( 'Service Account JSON', 'informatiq-smart-pricing' ),
array( $this, 'render_service_account_field' ),
'informatiq_sp_oauth_credentials',
__( 'OAuth 2.0 Credentials', 'informatiq-smart-pricing' ),
array( $this, 'render_oauth_credentials_field' ),
'informatiq_sp_settings',
'informatiq_sp_google_settings'
);
add_settings_field(
'informatiq_sp_authorization',
__( 'Google Authorization', 'informatiq-smart-pricing' ),
array( $this, 'render_authorization_field' ),
'informatiq_sp_settings',
'informatiq_sp_google_settings'
);
@@ -141,6 +154,108 @@ class Informatiq_SP_Admin {
);
}
/**
* Handle OAuth callback from Google.
*/
public function handle_oauth_callback() {
// Check if this is an OAuth callback.
if ( ! isset( $_GET['informatiq_sp_oauth'] ) || $_GET['informatiq_sp_oauth'] !== 'callback' ) {
return;
}
// Verify state parameter.
$state = isset( $_GET['state'] ) ? sanitize_text_field( $_GET['state'] ) : '';
$saved_state = get_transient( 'informatiq_sp_oauth_state' );
if ( empty( $state ) || $state !== $saved_state ) {
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
__( 'OAuth state mismatch. Please try again.', 'informatiq-smart-pricing' ),
'error'
);
return;
}
delete_transient( 'informatiq_sp_oauth_state' );
// Check for errors.
if ( isset( $_GET['error'] ) ) {
$error = sanitize_text_field( $_GET['error'] );
$error_desc = isset( $_GET['error_description'] ) ? sanitize_text_field( $_GET['error_description'] ) : $error;
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
sprintf( __( 'Authorization failed: %s', 'informatiq-smart-pricing' ), $error_desc ),
'error'
);
return;
}
// Get authorization code.
$code = isset( $_GET['code'] ) ? sanitize_text_field( $_GET['code'] ) : '';
if ( empty( $code ) ) {
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
__( 'No authorization code received.', 'informatiq-smart-pricing' ),
'error'
);
return;
}
// Exchange code for tokens.
$client_id = get_option( 'informatiq_sp_client_id' );
$client_secret = get_option( 'informatiq_sp_client_secret' );
$redirect_uri = $this->get_oauth_redirect_uri();
try {
$tokens = Informatiq_SP_Google_API::exchange_code_for_tokens(
$code,
$client_id,
$client_secret,
$redirect_uri
);
// Save refresh token.
update_option( 'informatiq_sp_refresh_token', $tokens['refresh_token'] );
$this->logger->info( 'Google OAuth authorization successful' );
// Redirect to settings page with success message.
wp_redirect( admin_url( 'admin.php?page=informatiq-smart-pricing&oauth=success' ) );
exit;
} catch ( Exception $e ) {
$this->logger->error( 'OAuth token exchange failed: ' . $e->getMessage() );
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_oauth_error',
sprintf( __( 'Token exchange failed: %s', 'informatiq-smart-pricing' ), $e->getMessage() ),
'error'
);
}
}
/**
* Get OAuth redirect URI.
*
* @return string Redirect URI.
*/
private function get_oauth_redirect_uri() {
return admin_url( 'admin.php?page=informatiq-smart-pricing&informatiq_sp_oauth=callback' );
}
/**
* Check if plugin is authorized.
*
* @return bool True if authorized.
*/
private function is_authorized() {
return ! empty( get_option( 'informatiq_sp_refresh_token' ) );
}
/**
* Render admin page.
*/
@@ -149,6 +264,16 @@ class Informatiq_SP_Admin {
return;
}
// Check for OAuth success message.
if ( isset( $_GET['oauth'] ) && $_GET['oauth'] === 'success' ) {
add_settings_error(
'informatiq_sp_messages',
'informatiq_sp_message',
__( 'Google authorization successful!', 'informatiq-smart-pricing' ),
'success'
);
}
// Check if form was submitted.
if ( isset( $_GET['settings-updated'] ) ) {
// Update cron schedule if frequency changed.
@@ -185,14 +310,20 @@ class Informatiq_SP_Admin {
<h2><?php esc_html_e( 'Manual Actions', 'informatiq-smart-pricing' ); ?></h2>
<p>
<button type="button" class="button button-secondary" id="informatiq-sp-test-connection">
<button type="button" class="button button-secondary" id="informatiq-sp-test-connection" <?php disabled( ! $this->is_authorized() ); ?>>
<?php esc_html_e( 'Test Google API Connection', 'informatiq-smart-pricing' ); ?>
</button>
<button type="button" class="button button-primary" id="informatiq-sp-manual-sync">
<button type="button" class="button button-primary" id="informatiq-sp-manual-sync" <?php disabled( ! $this->is_authorized() ); ?>>
<?php esc_html_e( 'Run Manual Sync Now', 'informatiq-smart-pricing' ); ?>
</button>
</p>
<?php if ( ! $this->is_authorized() ) : ?>
<p class="description" style="color: #d63638;">
<?php esc_html_e( 'Please authorize with Google first to use these features.', 'informatiq-smart-pricing' ); ?>
</p>
<?php endif; ?>
<div id="informatiq-sp-sync-status" class="notice" style="display: none;"></div>
</div>
@@ -217,7 +348,7 @@ class Informatiq_SP_Admin {
* Render Google settings section description.
*/
public function render_google_settings_section() {
echo '<p>' . esc_html__( 'Configure your Google Merchant Center API credentials.', 'informatiq-smart-pricing' ) . '</p>';
echo '<p>' . esc_html__( 'Configure your Google Merchant Center API credentials using OAuth 2.0.', 'informatiq-smart-pricing' ) . '</p>';
}
/**
@@ -243,25 +374,100 @@ class Informatiq_SP_Admin {
<input type="text" name="informatiq_sp_merchant_id" id="informatiq_sp_merchant_id"
value="<?php echo esc_attr( $value ); ?>" class="regular-text">
<p class="description">
<?php esc_html_e( 'Your Google Merchant Center ID.', 'informatiq-smart-pricing' ); ?>
<?php esc_html_e( 'Your Google Merchant Center ID (found in Merchant Center settings).', 'informatiq-smart-pricing' ); ?>
</p>
<?php
}
/**
* Render service account field.
* Render OAuth credentials field.
*/
public function render_service_account_field() {
$value = get_option( 'informatiq_sp_service_account' );
public function render_oauth_credentials_field() {
$client_id = get_option( 'informatiq_sp_client_id' );
$client_secret = get_option( 'informatiq_sp_client_secret' );
?>
<textarea name="informatiq_sp_service_account" id="informatiq_sp_service_account"
rows="10" class="large-text code"><?php echo esc_textarea( $value ); ?></textarea>
<p class="description">
<?php esc_html_e( 'Paste your Google Service Account JSON key here.', 'informatiq-smart-pricing' ); ?>
</p>
<div class="informatiq-sp-oauth-credentials">
<p>
<label for="informatiq_sp_client_id"><strong><?php esc_html_e( 'Client ID:', 'informatiq-smart-pricing' ); ?></strong></label><br>
<input type="text" name="informatiq_sp_client_id" id="informatiq_sp_client_id"
value="<?php echo esc_attr( $client_id ); ?>" class="large-text">
</p>
<p>
<label for="informatiq_sp_client_secret"><strong><?php esc_html_e( 'Client Secret:', 'informatiq-smart-pricing' ); ?></strong></label><br>
<input type="password" name="informatiq_sp_client_secret" id="informatiq_sp_client_secret"
value="<?php echo esc_attr( $client_secret ); ?>" class="regular-text">
</p>
<p class="description">
<?php
printf(
/* translators: %s: redirect URI */
esc_html__( 'Create OAuth 2.0 credentials in Google Cloud Console. Use this as your Authorized redirect URI: %s', 'informatiq-smart-pricing' ),
'<code>' . esc_html( $this->get_oauth_redirect_uri() ) . '</code>'
);
?>
</p>
</div>
<?php
}
/**
* Render authorization field.
*/
public function render_authorization_field() {
$client_id = get_option( 'informatiq_sp_client_id' );
$client_secret = get_option( 'informatiq_sp_client_secret' );
$is_authorized = $this->is_authorized();
$has_credentials = ! empty( $client_id ) && ! empty( $client_secret );
if ( $is_authorized ) {
?>
<div class="informatiq-sp-auth-status authorized">
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
<strong style="color: #00a32a;"><?php esc_html_e( 'Authorized', 'informatiq-smart-pricing' ); ?></strong>
<p class="description">
<?php esc_html_e( 'Plugin is authorized to access your Google Merchant Center.', 'informatiq-smart-pricing' ); ?>
</p>
<p>
<button type="button" class="button button-secondary" id="informatiq-sp-revoke-auth">
<?php esc_html_e( 'Revoke Authorization', 'informatiq-smart-pricing' ); ?>
</button>
</p>
</div>
<?php
} elseif ( $has_credentials ) {
// Generate authorization URL.
$state = wp_generate_password( 32, false );
set_transient( 'informatiq_sp_oauth_state', $state, 600 ); // 10 minutes.
$auth_url = Informatiq_SP_Google_API::get_authorization_url(
$client_id,
$this->get_oauth_redirect_uri(),
$state
);
?>
<div class="informatiq-sp-auth-status not-authorized">
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
<strong style="color: #dba617;"><?php esc_html_e( 'Not Authorized', 'informatiq-smart-pricing' ); ?></strong>
<p class="description">
<?php esc_html_e( 'Click the button below to authorize this plugin with your Google account.', 'informatiq-smart-pricing' ); ?>
</p>
<p>
<a href="<?php echo esc_url( $auth_url ); ?>" class="button button-primary">
<?php esc_html_e( 'Authorize with Google', 'informatiq-smart-pricing' ); ?>
</a>
</p>
</div>
<?php
} else {
?>
<div class="informatiq-sp-auth-status no-credentials">
<span class="dashicons dashicons-info" style="color: #72aee6;"></span>
<em><?php esc_html_e( 'Enter your OAuth credentials above and save settings first.', 'informatiq-smart-pricing' ); ?></em>
</div>
<?php
}
}
/**
* Render minimum margin field.
*/
@@ -418,12 +624,13 @@ class Informatiq_SP_Admin {
private function render_sidebar() {
?>
<div class="informatiq-sp-sidebar-box">
<h3><?php esc_html_e( 'How It Works', 'informatiq-smart-pricing' ); ?></h3>
<h3><?php esc_html_e( 'Setup Steps', 'informatiq-smart-pricing' ); ?></h3>
<ol>
<li><?php esc_html_e( 'Configure Google Merchant Center credentials', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Set your minimum profit margin', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Enable automatic updates', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Plugin automatically updates prices daily', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Create OAuth 2.0 credentials in Google Cloud Console', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Enter Client ID and Client Secret above', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Click "Authorize with Google"', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Enter your Merchant ID', 'informatiq-smart-pricing' ); ?></li>
<li><?php esc_html_e( 'Set your minimum margin and enable automation', 'informatiq-smart-pricing' ); ?></li>
</ol>
</div>
@@ -461,6 +668,10 @@ class Informatiq_SP_Admin {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
if ( ! $this->is_authorized() ) {
wp_send_json_error( array( 'message' => __( 'Please authorize with Google first.', 'informatiq-smart-pricing' ) ) );
}
try {
$scheduler = new Informatiq_SP_Scheduler( $this->price_updater );
$results = $scheduler->trigger_manual_update();
@@ -491,16 +702,28 @@ class Informatiq_SP_Admin {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
$merchant_id = get_option( 'informatiq_sp_merchant_id' );
$service_account = get_option( 'informatiq_sp_service_account' );
$merchant_id = get_option( 'informatiq_sp_merchant_id' );
$client_id = get_option( 'informatiq_sp_client_id' );
$client_secret = get_option( 'informatiq_sp_client_secret' );
$refresh_token = get_option( 'informatiq_sp_refresh_token' );
if ( empty( $merchant_id ) || empty( $service_account ) ) {
wp_send_json_error( array( 'message' => __( 'Please configure Google Merchant settings first.', 'informatiq-smart-pricing' ) ) );
if ( empty( $merchant_id ) ) {
wp_send_json_error( array( 'message' => __( 'Please enter your Merchant ID.', 'informatiq-smart-pricing' ) ) );
}
if ( empty( $refresh_token ) ) {
wp_send_json_error( array( 'message' => __( 'Please authorize with Google first.', 'informatiq-smart-pricing' ) ) );
}
try {
$google_api = new Informatiq_SP_Google_API( $merchant_id, $service_account, $this->logger );
$success = $google_api->test_connection();
$google_api = new Informatiq_SP_Google_API(
$merchant_id,
$client_id,
$client_secret,
$refresh_token,
$this->logger
);
$success = $google_api->test_connection();
if ( $success ) {
wp_send_json_success( array( 'message' => __( 'Connection successful!', 'informatiq-smart-pricing' ) ) );
@@ -512,6 +735,23 @@ class Informatiq_SP_Admin {
}
}
/**
* Handle revoke authorization AJAX request.
*/
public function handle_revoke_auth() {
check_ajax_referer( 'informatiq_sp_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'informatiq-smart-pricing' ) ) );
}
delete_option( 'informatiq_sp_refresh_token' );
$this->logger->info( 'Google OAuth authorization revoked' );
wp_send_json_success( array( 'message' => __( 'Authorization revoked.', 'informatiq-smart-pricing' ) ) );
}
/**
* Enqueue admin assets.
*
@@ -544,8 +784,10 @@ class Informatiq_SP_Admin {
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'informatiq_sp_admin' ),
'strings' => array(
'syncInProgress' => __( 'Sync in progress...', 'informatiq-smart-pricing' ),
'testInProgress' => __( 'Testing connection...', 'informatiq-smart-pricing' ),
'syncInProgress' => __( 'Sync in progress...', 'informatiq-smart-pricing' ),
'testInProgress' => __( 'Testing connection...', 'informatiq-smart-pricing' ),
'revokeConfirm' => __( 'Are you sure you want to revoke Google authorization?', 'informatiq-smart-pricing' ),
'revokeInProgress' => __( 'Revoking...', 'informatiq-smart-pricing' ),
),
)
);