• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » WordPress Development Recipe: Secure token-based API authentication for Shopify headless API in custom plugins

WordPress Development Recipe: Secure token-based API authentication for Shopify headless API in custom plugins

Setting Up Secure Token-Based Authentication for Shopify Headless APIs in WordPress

Integrating Shopify’s headless APIs into a WordPress site requires robust authentication. This recipe details how to implement secure, token-based authentication within a custom WordPress plugin, leveraging OAuth 2.0 for secure token exchange and management. We’ll focus on the server-side flow within WordPress, ensuring sensitive API keys and tokens are never exposed to the client.

Prerequisites and Initial Setup

Before diving into the code, ensure you have:

  • A Shopify Partner account and a development store.
  • A registered Shopify App (private or custom app) with the necessary API scopes (e.g., read_products, write_orders). Note down your App’s API Key and Secret Key.
  • A WordPress installation where you’ll develop your custom plugin.

Registering a Callback URL in Shopify

Shopify needs a designated URL within your WordPress site to redirect users after they authorize your app. This URL will handle the token exchange.

In your Shopify App settings, under “App URL” and “Allowed redirection URL(s)”, add a URL like: https://your-wordpress-site.com/wp-admin/admin-ajax.php?action=shopify_auth_callback. This uses WordPress’s AJAX handler for a clean, integrated callback.

WordPress Plugin Structure

We’ll create a basic plugin structure. Assume your plugin is named my-shopify-connector.

  • Create a directory: wp-content/plugins/my-shopify-connector/
  • Create the main plugin file: wp-content/plugins/my-shopify-connector/my-shopify-connector.php

Initiating the OAuth Flow

The process begins when a user (or an admin) needs to connect their Shopify store. This typically involves a button or link in the WordPress admin area that redirects to Shopify’s authorization URL.

We’ll store your Shopify App’s API Key and Secret Key as constants or in WordPress options. For this example, we’ll use constants defined in wp-config.php for better security.

Storing Credentials Securely

Add these to your wp-config.php file:

define( 'SHOPIFY_API_KEY', 'YOUR_SHOPIFY_API_KEY' );
define( 'SHOPIFY_API_SECRET', 'YOUR_SHOPIFY_API_SECRET' );
define( 'SHOPIFY_APP_SCOPES', 'read_products,write_orders' ); // Comma-separated list of scopes

Generating the Authorization URL

In your main plugin file, create a function to generate the authorization URL. This URL will redirect the user to Shopify to grant permissions.

<?php
/*
Plugin Name: My Shopify Connector
Description: Connects to Shopify Headless API using OAuth 2.0.
Version: 1.0
Author: Your Name
*/

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

// Ensure Shopify API credentials are defined
if ( ! defined( 'SHOPIFY_API_KEY' ) || ! defined( 'SHOPIFY_API_SECRET' ) || ! defined( 'SHOPIFY_APP_SCOPES' ) ) {
    // In a real plugin, you might want to display an admin notice or log an error.
    // For this example, we'll assume they are defined.
    // error_log('Shopify API credentials are not defined in wp-config.php');
}

/**
 * Generates the Shopify authorization URL.
 *
 * @param string $shop_domain The Shopify store domain (e.g., 'your-store.myshopify.com').
 * @return string The authorization URL.
 */
function msc_get_shopify_auth_url( $shop_domain ) {
    if ( empty( $shop_domain ) ) {
        return '#'; // Or handle error appropriately
    }

    $shop_domain = sanitize_text_field( $shop_domain );
    // Ensure the domain ends with .myshopify.com
    if ( strpos( $shop_domain, '.myshopify.com' ) === false ) {
        $shop_domain .= '.myshopify.com';
    }

    $callback_url = admin_url( 'admin-ajax.php?action=shopify_auth_callback' );

    $auth_params = array(
        'client_id'     => SHOPIFY_API_KEY,
        'scope'         => SHOPIFY_APP_SCOPES,
        'redirect_uri'  => $callback_url,
    );

    $auth_url = add_query_arg( $auth_params, 'https://' . $shop_domain . '/admin/oauth/authorize' );

    return esc_url_raw( $auth_url );
}

// Example usage: Add a button in the WordPress admin
add_action( 'admin_menu', 'msc_add_admin_menu' );
function msc_add_admin_menu() {
    add_menu_page(
        'Shopify Connector',
        'Shopify Connector',
        'manage_options',
        'my-shopify-connector',
        'msc_render_admin_page',
        'dashicons-shopify',
        80
    );
}

function msc_render_admin_page() {
    ?>
    <div class="wrap">
        <h1>Shopify Connector</h1>
        <p>Connect your Shopify store to WordPress.</p>
        <form method="post" action="">
            <table class="form-table">
                <tr>
                    <th><label for="shopify_shop_domain">Shopify Store Domain</label></th>
                    <td><input type="text" id="shopify_shop_domain" name="shopify_shop_domain" value="" placeholder="your-store.myshopify.com" required></td>
                </tr>
            </table>
            <p class="submit">
                <button type="submit" name="connect_shopify" class="button button-primary">Connect to Shopify</button>
            </p>
        </form>
    </div>
    


Handling the Callback and Token Exchange

When Shopify redirects back to your site, the admin-ajax.php endpoint will receive query parameters, including a code (authorization code) and the shop domain.

We need to hook into the wp_ajax_shopify_auth_callback action to process this.

<?php
// ... (previous code)

/**
 * Handles the Shopify OAuth callback.
 */
add_action( 'wp_ajax_shopify_auth_callback', 'msc_handle_shopify_callback' );
function msc_handle_shopify_callback() {
    // Retrieve the shop domain stored earlier
    $shop_domain = get_transient( 'msc_shopify_shop_domain_for_auth_' . get_current_user_id() );
    delete_transient( 'msc_shopify_shop_domain_for_auth_' . get_current_user_id() ); // Clean up transient

    if ( empty( $shop_domain ) || empty( $_GET['code'] ) || empty( $_GET['shop'] ) ) {
        // Handle error: missing parameters or shop domain not found
        wp_die( 'Shopify authentication failed. Missing parameters or shop domain.', 'Shopify Auth Error', array( 'response' => 400 ) );
    }

    // Sanitize and verify the shop domain
    $shop_domain_from_get = sanitize_text_field( $_GET['shop'] );
    if ( $shop_domain_from_get !== $shop_domain ) {
        wp_die( 'Shopify authentication failed. Mismatched shop domain.', 'Shopify Auth Error', array( 'response' => 400 ) );
    }

    // Ensure the domain ends with .myshopify.com
    if ( strpos( $shop_domain, '.myshopify.com' ) === false ) {
        $shop_domain .= '.myshopify.com';
    }

    $auth_code = sanitize_text_field( $_GET['code'] );

    // Prepare the request to exchange the authorization code for an access token
    $token_exchange_url = 'https://' . $shop_domain . '/admin/oauth/access_token';
    $request_body = array(
        'client_id'     => SHOPIFY_API_KEY,
        'client_secret' => SHOPIFY_API_SECRET,
        'code'          => $auth_code,
    );

    // Use WordPress HTTP API for the request
    $response = wp_remote_post( $token_exchange_url, array(
        'method'    => 'POST',
        'body'      => $request_body,
        'headers'   => array(
            'Content-Type' => 'application/x-www-form-urlencoded',
        ),
        'timeout'   => 60, // Adjust timeout as needed
        'sslverify' => true, // Important for security
    ) );

    if ( is_wp_error( $response ) ) {
        // Handle HTTP request error
        wp_die( 'Shopify authentication failed: ' . $response->get_error_message(), 'Shopify Auth Error', array( 'response' => 500 ) );
    }

    $response_code = wp_remote_retrieve_response_code( $response );
    $response_body = wp_remote_retrieve_body( $response );
    $token_data = json_decode( $response_body, true );

    if ( $response_code !== 200 || ! $token_data || isset( $token_data['error'] ) ) {
        // Handle API error response from Shopify
        $error_message = isset( $token_data['error_description'] ) ? $token_data['error_description'] : ( isset( $token_data['error'] ) ? $token_data['error'] : 'Unknown error' );
        wp_die( 'Shopify token exchange failed: ' . esc_html( $error_message ), 'Shopify Auth Error', array( 'response' => $response_code ) );
    }

    // Successfully obtained access token and other details
    $access_token = $token_data['access_token'];
    $scope = $token_data['scope']; // The scopes granted by the user

    // Store the access token securely. Use WordPress options or a custom database table.
    // For simplicity, we'll use options here, but consider encryption for production.
    $option_name = 'msc_shopify_access_token_' . md5( $shop_domain ); // Use MD5 hash of domain as part of option name
    update_option( $option_name, array(
        'shop_domain'   => $shop_domain,
        'access_token'  => $access_token,
        'scope'         => $scope,
        'timestamp'     => time(), // Store timestamp for potential token refresh logic
    ) );

    // Redirect to a success page or back to the admin menu
    wp_redirect( admin_url( 'admin.php?page=my-shopify-connector&shopify_connected=true' ) );
    exit;
}
?>

Storing and Managing Access Tokens

Access tokens should be stored securely. Using WordPress options (update_option, get_option) is a common approach. For enhanced security, consider encrypting the access token before storing it, especially if it's sensitive or has write permissions.

The example above stores an array containing the shop domain, access token, granted scopes, and a timestamp. The option name is prefixed and uses an MD5 hash of the shop domain to allow storing tokens for multiple stores.

Token Expiration and Refresh

Shopify's OAuth 2.0 access tokens for custom apps are typically long-lived (e.g., 2 years). However, for public apps, tokens might expire and require refresh. Shopify's API documentation should be consulted for the specific app type and token lifecycle. If refresh tokens are provided or required, you'll need to implement a separate flow to obtain new access tokens using the refresh token.

Making Authenticated API Requests

Now that we have an access token, we can make authenticated requests to the Shopify API.

<?php
// ... (previous code)

/**
 * Retrieves the stored Shopify access token for a given shop domain.
 *
 * @param string $shop_domain The Shopify store domain.
 * @return array|false The stored token data or false if not found.
 */
function msc_get_stored_shopify_token( $shop_domain ) {
    if ( empty( $shop_domain ) ) {
        return false;
    }
    $shop_domain = sanitize_text_field( $shop_domain );
    if ( strpos( $shop_domain, '.myshopify.com' ) === false ) {
        $shop_domain .= '.myshopify.com';
    }
    $option_name = 'msc_shopify_access_token_' . md5( $shop_domain );
    return get_option( $option_name, false );
}

/**
 * Makes an authenticated request to the Shopify Admin API.
 *
 * @param string $shop_domain The Shopify store domain.
 * @param string $endpoint The API endpoint (e.g., '/admin/api/2023-10/products.json').
 * @param string $method HTTP method (GET, POST, PUT, DELETE).
 * @param array $data Optional data for POST/PUT requests.
 * @return array|WP_Error The API response data or a WP_Error object on failure.
 */
function msc_shopify_api_request( $shop_domain, $endpoint, $method = 'GET', $data = array() ) {
    $token_data = msc_get_stored_shopify_token( $shop_domain );

    if ( ! $token_data || empty( $token_data['access_token'] ) ) {
        return new WP_Error( 'shopify_auth_error', 'Shopify access token not found or invalid.' );
    }

    $access_token = $token_data['access_token'];
    $api_version = '2023-10'; // Use a specific API version

    // Ensure the shop domain has the .myshopify.com suffix
    if ( strpos( $shop_domain, '.myshopify.com' ) === false ) {
        $shop_domain .= '.myshopify.com';
    }

    $api_url = 'https://' . $shop_domain . '/admin/api/' . $api_version . $endpoint;

    $headers = array(
        'X-Shopify-Access-Token' => $access_token,
        'Content-Type'           => 'application/json',
        'Accept'                 => 'application/json',
    );

    $args = array(
        'method'    => strtoupper( $method ),
        'headers'   => $headers,
        'timeout'   => 60,
        'sslverify' => true,
    );

    if ( $method === 'POST' || $method === 'PUT' || $method === 'DELETE' ) {
        if ( ! empty( $data ) ) {
            $args['body'] = json_encode( $data );
        }
    }

    $response = wp_remote_request( $api_url, $args );

    if ( is_wp_error( $response ) ) {
        return $response; // Return WP_Error object
    }

    $response_code = wp_remote_retrieve_response_code( $response );
    $response_body = wp_remote_retrieve_body( $response );
    $decoded_body = json_decode( $response_body, true );

    if ( $response_code >= 400 ) {
        // Handle API errors (e.g., 401 Unauthorized, 403 Forbidden, 429 Too Many Requests)
        $error_message = isset( $decoded_body['errors'] ) ? $decoded_body['errors'] : $response_body;
        return new WP_Error( 'shopify_api_error', 'Shopify API Error (' . $response_code . '): ' . print_r( $error_message, true ) );
    }

    return $decoded_body; // Return decoded JSON response
}

// Example of how to use the API request function
function msc_get_products_from_shopify( $shop_domain ) {
    $products = msc_shopify_api_request( $shop_domain, '/admin/api/2023-10/products.json', 'GET' );

    if ( is_wp_error( $products ) ) {
        // Handle error, e.g., display an admin notice
        error_log( 'Failed to get Shopify products: ' . $products->get_error_message() );
        return false;
    }

    // Process the $products array
    if ( ! empty( $products['products'] ) ) {
        foreach ( $products['products'] as $product ) {
            // Do something with each product
            // echo '<pre>' . print_r( $product['title'], true ) . '</pre>';
        }
    }
    return $products;
}

// Add a button to trigger fetching products on the admin page
add_action( 'admin_notices', 'msc_display_connection_status' );
function msc_display_connection_status() {
    if ( isset( $_GET['page'] ) && $_GET['page'] === 'my-shopify-connector' ) {
        $connected_shop_domain = ''; // You'd typically retrieve this from options
        // For demonstration, let's assume we have a stored domain
        // In a real scenario, loop through stored options to find a connected store
        $all_options = wp_load_alloptions();
        foreach ($all_options as $key => $value) {
            if (strpos($key, 'msc_shopify_access_token_') === 0 && is_array($value) && isset($value['shop_domain'])) {
                $connected_shop_domain = $value['shop_domain'];
                break;
            }
        }

        if ( $connected_shop_domain ) {
            echo '<div class="notice notice-success is-dismissible"><p>Successfully connected to Shopify store: ' . esc_html( $connected_shop_domain ) . '</p></div>';
            echo '<p><a href="' . esc_url( add_query_arg( 'fetch_products', 'true', admin_url( 'admin.php?page=my-shopify-connector' ) ) ) . '" class="button button-secondary">Fetch Products</a></p>';

            if ( isset( $_GET['shopify_connected'] ) && $_GET['shopify_connected'] === 'true' ) {
                // This notice is shown after successful callback redirection
                // The success notice above will also be shown.
            }
        } else {
            echo '<div class="notice notice-warning is-dismissible"><p>Shopify store not connected.</p></div>';
        }

        if ( isset( $_GET['fetch_products'] ) && $_GET['fetch_products'] === 'true' && $connected_shop_domain ) {
            echo '<div class="wrap"><h3>Products</h3><pre>';
            $products_data = msc_get_products_from_shopify( $connected_shop_domain );
            if ( is_wp_error( $products_data ) ) {
                echo 'Error: ' . esc_html( $products_data->get_error_message() );
            } elseif ( ! empty( $products_data['products'] ) ) {
                echo esc_html( count( $products_data['products'] ) ) . ' products found:<br>';
                foreach ( $products_data['products'] as $product ) {
                    echo '- ' . esc_html( $product['title'] ) . ' (ID: ' . esc_html( $product['id'] ) . ')<br>';
                }
            } else {
                echo 'No products found or an empty response.';
            }
            echo '</pre></div>';
        }
    }
}
?>

Security Considerations and Best Practices

  • Never expose API secrets directly in client-side code. All authentication logic should reside on the server (WordPress backend).
  • Use HTTPS for all communication. This is crucial for protecting credentials and data in transit.
  • Sanitize all user inputs. Especially the shop domain and any data sent to the Shopify API.
  • Validate redirect URIs. Ensure that the shop domain received in the callback matches the one initiated the OAuth flow.
  • Store sensitive tokens securely. Consider encrypting access tokens in the WordPress database. WordPress's built-in encryption functions (if available and configured) or a dedicated library can be used.
  • Implement proper error handling. Log errors and provide informative feedback to the user without revealing sensitive details.
  • Use specific API versions. Always specify the API version in your requests to avoid unexpected changes.
  • Manage scopes carefully. Request only the necessary scopes for your plugin's functionality.
  • Regularly review Shopify app permissions. Ensure only authorized access is granted.

Further Enhancements

  • Token Refresh Logic: Implement a mechanism to refresh access tokens if they expire (especially for public apps).
  • Webhooks: Set up Shopify webhooks to receive real-time updates from Shopify (e.g., product creation, order fulfillment) and process them within WordPress.
  • Admin Notices: Use WordPress admin notices for clearer feedback on connection status, errors, and successful operations.
  • Settings API: For a more robust plugin, utilize the WordPress Settings API to manage Shopify app settings and connected stores.
  • Object Cache: Cache API responses where appropriate to reduce the number of requests to Shopify and improve performance.
  • Rate Limiting: Be mindful of Shopify's API rate limits and implement strategies to handle them gracefully (e.g., exponential backoff).

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Carbon Fields custom wrappers
  • Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (48)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (182)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala