How to implement custom Shortcode API endpoints with token authentication in Gutenberg blocks
Leveraging WordPress REST API for Custom Shortcode Endpoints
While WordPress’s Shortcode API is traditionally used for in-content rendering, its integration with the REST API opens up powerful possibilities for dynamic data fetching and manipulation within Gutenberg blocks. This approach allows us to create custom API endpoints that can be securely accessed by JavaScript running in the block editor or on the frontend, enabling richer, more interactive user experiences. We’ll focus on building a robust solution that includes token-based authentication for enhanced security.
Setting Up a Custom REST API Endpoint
The foundation of our solution lies in registering a custom REST API endpoint. This is achieved using the register_rest_route function within a WordPress plugin or theme’s functions.php file. For this example, we’ll create an endpoint to fetch a list of “featured products” (a hypothetical custom post type).
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/featured-products', array(
'methods' => 'GET',
'callback' => 'myplugin_get_featured_products',
'permission_callback' => '__return_true', // Placeholder for authentication
) );
} );
function myplugin_get_featured_products( WP_REST_Request $request ) {
// In a real-world scenario, you'd query your custom post type or other data sources.
// For demonstration, we'll return dummy data.
$products = array(
array(
'id' => 101,
'title' => 'Awesome Gadget',
'price' => '$99.99',
'image' => 'https://example.com/images/gadget.jpg',
),
array(
'id' => 102,
'title' => 'Super Widget',
'price' => '$49.50',
'image' => 'https://example.com/images/widget.jpg',
),
);
return new WP_REST_Response( $products, 200 );
}
In this code:
rest_api_inithook ensures our route is registered when the API is initialized.register_rest_routedefines the namespace (‘myplugin/v1’), the route (‘/featured-products’), the HTTP method (‘GET’), the callback function, and apermission_callback.- The
myplugin_get_featured_productsfunction simulates fetching data. In production, this would involve WP_Query for custom post types, options, or external API calls. WP_REST_Responseis used to return data with an appropriate HTTP status code.
Implementing Token-Based Authentication
Directly returning __return_true for permission_callback is insecure. We need to implement a mechanism to verify that only authorized requests can access our endpoint. A common and effective method is token-based authentication. This involves generating a unique token for each user (or for specific API access) and requiring it to be passed in the request headers.
Generating and Storing API Tokens
Tokens can be generated and stored in user meta. A simple approach is to use a unique identifier like a UUID. We’ll create a function to generate a token and a function to retrieve it.
// Function to generate a new API token
function myplugin_generate_api_token() {
return bin2hex( random_bytes( 32 ) ); // Generates a 64-character hex token
}
// Function to get a user's API token
function myplugin_get_user_api_token( $user_id ) {
return get_user_meta( $user_id, 'myplugin_api_token', true );
}
// Function to set a user's API token (e.g., in user profile)
function myplugin_set_user_api_token( $user_id, $token ) {
update_user_meta( $user_id, 'myplugin_api_token', $token );
}
// Example: Generate and set a token for the current user (for testing)
// In a real app, this would be triggered by an admin action or user request.
// add_action( 'init', function() {
// if ( is_user_logged_in() && ! myplugin_get_user_api_token( get_current_user_id() ) ) {
// $token = myplugin_generate_api_token();
// myplugin_set_user_api_token( get_current_user_id(), $token );
// error_log( 'Generated API Token for user ' . get_current_user_id() . ': ' . $token );
// }
// });
Securing the REST API Endpoint with Token Authentication
Now, we’ll modify the permission_callback to check for the presence and validity of the API token in the request headers. We’ll expect the token in the X-API-Token header.
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/featured-products', array(
'methods' => 'GET',
'callback' => 'myplugin_get_featured_products',
'permission_callback' => 'myplugin_authenticate_api_request',
) );
} );
function myplugin_authenticate_api_request( WP_REST_Request $request ) {
$token = $request->get_header( 'X-API-Token' );
if ( empty( $token ) ) {
return new WP_Error( 'rest_not_logged_in', 'API token is required.', array( 'status' => 401 ) );
}
// Find the user associated with this token
global $wpdb;
$user_id = $wpdb->get_var( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = 'myplugin_api_token' AND meta_value = %s",
$token
) );
if ( ! $user_id ) {
return new WP_Error( 'rest_invalid_token', 'Invalid API token.', array( 'status' => 401 ) );
}
// Optionally, check if the user is active/has necessary capabilities
$user = get_user_by( 'id', $user_id );
if ( ! $user || ! $user->exists() || ! user_can( $user_id, 'read' ) ) { // 'read' capability is a basic check
return new WP_Error( 'rest_user_unauthorized', 'User associated with token is not authorized.', array( 'status' => 403 ) );
}
// Set the authenticated user for the request
wp_set_current_user( $user_id, $user->user_login );
return true; // Authentication successful
}
// The myplugin_get_featured_products function remains the same as before.
function myplugin_get_featured_products( WP_REST_Request $request ) {
// ... (previous implementation) ...
$products = array(
array(
'id' => 101,
'title' => 'Awesome Gadget',
'price' => '$99.99',
'image' => 'https://example.com/images/gadget.jpg',
),
array(
'id' => 102,
'title' => 'Super Widget',
'price' => '$49.50',
'image' => 'https://example.com/images/widget.jpg',
),
);
return new WP_REST_Response( $products, 200 );
}
In myplugin_authenticate_api_request:
- We retrieve the token from the
X-API-Tokenheader using$request->get_header(). - If the token is missing, we return a
401 Unauthorizederror. - We perform a direct database query using
$wpdbto find theuser_idassociated with the provided token. This is more efficient than iterating through all users. - If no user is found for the token, it’s invalid, and we return a
401 Unauthorizederror. - We perform a basic capability check (e.g.,
user_can( $user_id, 'read' )) to ensure the user is active and has at least minimal permissions. - If authentication is successful, we set the current user using
wp_set_current_user(), making their data available within the callback if needed. - The function returns
trueon success, allowing the main callback to execute.
Integrating with Gutenberg Blocks
Now, let’s see how to consume this authenticated endpoint from a Gutenberg block. We’ll create a simple block that fetches and displays the featured products.
Block Registration and JavaScript
First, ensure your block is registered correctly. The core logic will reside in the block’s JavaScript file.
// src/index.js (or your block's main JS file)
import { registerBlockType } from '@wordpress/blocks';
import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
// Set up a custom route for apiFetch
const restApiSettings = {
namespace: 'myplugin/v1',
};
apiFetch.use( apiFetch.createRootURLMiddleware( '/wp-json/' ) );
apiFetch.use( apiFetch.createNonceMiddleware( '/wp-json/' ) ); // For general WP REST API, not strictly needed for token auth but good practice.
apiFetch.use( apiFetch.createHeadersMiddleware( {
'X-API-Token': 'YOUR_STATIC_OR_DYNAMIC_TOKEN_HERE', // This needs to be dynamic in a real app
} ) );
registerBlockType( 'myplugin/featured-products', {
title: __( 'Featured Products', 'myplugin' ),
icon: 'star-filled',
category: 'widgets',
edit: function( props ) {
const [ products, setProducts ] = useState( [] );
const [ isLoading, setIsLoading ] = useState( true );
const [ error, setError ] = useState( null );
useEffect( () => {
const fetchProducts = async () => {
setIsLoading( true );
setError( null );
try {
// Construct the full route path
const response = await apiFetch( {
path: '/myplugin/v1/featured-products',
} );
setProducts( response );
} catch ( err ) {
setError( err );
console.error( 'Error fetching featured products:', err );
} finally {
setIsLoading( false );
}
};
fetchProducts();
}, [] );
if ( isLoading ) {
return __( 'Loading products...', 'myplugin' );
}
if ( error ) {
return __( 'Error loading products.', 'myplugin' );
}
if ( products.length === 0 ) {
return __( 'No featured products found.', 'myplugin' );
}
return (
<div className="featured-products-block">
<h3>{ __( 'Featured Products', 'myplugin' ) }</h3>
<ul>
{ products.map( ( product ) => (
<li key={ product.id }>
<img src={ product.image } alt={ product.title } width="50" />
<strong>{ product.title }</strong> - { product.price }
</li>
) ) }
</ul>
</div>
);
},
save: function( props ) {
// For simplicity, we'll render a placeholder in the editor and rely on
// server-side rendering or a frontend JS fetch for the actual content.
// In a real scenario, you might fetch and render here, or use a shortcode.
return null; // Or render static HTML if not dynamically fetched on frontend
},
} );
Key points for the JavaScript:
- We use
@wordpress/api-fetch, the recommended way to interact with the WordPress REST API from JavaScript. - We configure
apiFetchto include our custom API token in theX-API-Tokenheader. Crucially, in a production environment, this token should NOT be hardcoded. It needs to be dynamically retrieved and passed. This could involve:- Storing it in user settings accessible via
wp_localize_scriptfor logged-in users. - Generating a temporary token on the server and passing it to the frontend.
- Using a more sophisticated OAuth or JWT flow.
- Storing it in user settings accessible via
- The
pathforapiFetchdirectly corresponds to our registered REST route:/myplugin/v1/featured-products. - We use React hooks (
useState,useEffect) to manage the state of our data fetching (loading, error, data). - The
savefunction is left asnullor a static placeholder because the data is fetched dynamically. If you need this data to be present in the initial HTML for SEO or performance, you would typically use server-side rendering (PHP) for the block or ensure the frontend JavaScript fetches and renders it on page load.
Handling Dynamic Tokens in JavaScript
Hardcoding tokens is a security risk. Here’s a more secure approach for passing tokens to the frontend JavaScript:
// In your plugin's main PHP file or a dedicated script enqueuing file
add_action( 'enqueue_block_editor_assets', 'myplugin_enqueue_block_assets' );
add_action( 'wp_enqueue_scripts', 'myplugin_enqueue_block_assets' ); // For frontend rendering if needed
function myplugin_enqueue_block_assets() {
// Get the current user's token. If not logged in, this might be null or a system-wide token.
$user_id = get_current_user_id();
$api_token = '';
if ( $user_id ) {
$api_token = myplugin_get_user_api_token( $user_id );
}
// Fallback for non-logged-in users or if no user token is found.
// This could be a system-wide token, or you might disallow access.
if ( empty( $api_token ) ) {
// Example: Fetch a system-wide token if one is configured.
// $api_token = get_option( 'myplugin_system_api_token' );
// For this example, we'll leave it empty if no user token exists.
}
// Localize the script with the token
wp_localize_script(
'myplugin-block-editor-script', // The handle of your registered block JS file
'mypluginBlockData',
array(
'apiToken' => $api_token,
// Add other data if needed
)
);
}
Then, in your JavaScript:
// src/index.js (updated)
import { registerBlockType } from '@wordpress/blocks';
import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
// Set up a custom route for apiFetch
const restApiSettings = {
namespace: 'myplugin/v1',
};
apiFetch.use( apiFetch.createRootURLMiddleware( '/wp-json/' ) );
// apiFetch.use( apiFetch.createNonceMiddleware( '/wp-json/' ) ); // May not be needed if token auth is primary
// Dynamically get the token from localized data
const apiToken = typeof mypluginBlockData !== 'undefined' ? mypluginBlockData.apiToken : '';
if ( apiToken ) {
apiFetch.use( apiFetch.createHeadersMiddleware( {
'X-API-Token': apiToken,
} ) );
} else {
// Handle cases where no token is available (e.g., show a message, disable block)
console.warn( 'API Token not available for Featured Products block.' );
}
registerBlockType( 'myplugin/featured-products', {
// ... (rest of the block registration) ...
edit: function( props ) {
const [ products, setProducts ] = useState( [] );
const [ isLoading, setIsLoading ] = useState( true );
const [ error, setError ] = useState( null );
useEffect( () => {
const fetchProducts = async () => {
setIsLoading( true );
setError( null );
try {
// Check if token is available before attempting fetch
if ( ! apiToken ) {
throw new Error( 'API token is missing.' );
}
const response = await apiFetch( {
path: '/myplugin/v1/featured-products',
} );
setProducts( response );
} catch ( err ) {
setError( err );
console.error( 'Error fetching featured products:', err );
} finally {
setIsLoading( false );
}
};
fetchProducts();
}, [] );
// ... (rendering logic remains similar) ...
if ( isLoading ) {
return __( 'Loading products...', 'myplugin' );
}
if ( error ) {
return __( `Error loading products: ${error.message}`, 'myplugin' );
}
if ( products.length === 0 ) {
return __( 'No featured products found.', 'myplugin' );
}
return (
<div className="featured-products-block">
<h3>{ __( 'Featured Products', 'myplugin' ) }</h3>
<ul>
{ products.map( ( product ) => (
<li key={ product.id }>
<img src={ product.image } alt={ product.title } width="50" />
<strong>{ product.title }</strong> - { product.price }
</li>
) ) }
</ul>
</div>
);
},
save: function( props ) {
// For dynamic content, the save function should ideally return null
// or a placeholder, and the content should be rendered on the frontend
// via PHP or client-side JS.
return null;
},
} );
Server-Side Rendering for Dynamic Blocks
For blocks that display dynamic data, especially if SEO is a concern, server-side rendering (SSR) is often preferred. This means the block’s HTML is generated on the server by PHP, and then sent to the browser.
// In your block's PHP registration file
register_block_type( 'myplugin/featured-products', array(
'editor_script' => 'myplugin-block-editor-script', // Your block's JS handle
'render_callback' => 'myplugin_render_featured_products_block',
// 'attributes' would be defined here if the block had configurable settings
) );
function myplugin_render_featured_products_block( $attributes ) {
// This function runs on the server.
// It needs to fetch data and render HTML.
// Authentication here is implicit if the REST API endpoint is called by the server.
// However, for direct rendering, we might need to re-authenticate or use a server-side fetch.
// For simplicity, we'll simulate fetching data again.
// In a real app, you might call your existing REST API endpoint from PHP,
// or have a dedicated server-side data fetching function.
// Example: Using a helper function that bypasses the need for client-side token
// This requires careful security considerations.
$products = myplugin_get_featured_products_server_side(); // A new function to fetch data server-side
if ( empty( $products ) ) {
return '<p>' . __( 'No featured products available.', 'myplugin' ) . '</p>';
}
$output = '<div class="featured-products-block-ssr">';
$output .= '<h3>' . __( 'Featured Products', 'myplugin' ) . '</h3>';
$output .= '<ul>';
foreach ( $products as $product ) {
$output .= '<li>';
$output .= '<img src="' . esc_url( $product['image'] ) . '" alt="' . esc_attr( $product['title'] ) . '" width="50" />';
$output .= '<strong>' . esc_html( $product['title'] ) . '</strong> - ' . esc_html( $product['price'] );
$output .= '</li>';
}
$output .= '</ul>';
$output .= '</div>';
return $output;
}
// A server-side data fetching function (similar to the REST API callback)
function myplugin_get_featured_products_server_side() {
// This function would ideally use the same data source as the REST API callback.
// It doesn't need to worry about 'X-API-Token' headers as it's running server-side.
// You might need to re-implement the data fetching logic here or call a shared service.
// For demonstration:
return array(
array(
'id' => 201,
'title' => 'Server Gadget',
'price' => '$120.00',
'image' => 'https://example.com/images/server-gadget.jpg',
),
array(
'id' => 202,
'title' => 'Server Widget',
'price' => '$60.75',
'image' => 'https://example.com/images/server-widget.jpg',
),
);
}
With SSR, the save function in your JavaScript block can return null, and the block will be rendered server-side. The render_callback function is responsible for generating the HTML output. Notice the use of esc_url(), esc_attr(), and esc_html() for security.
Security Considerations and Best Practices
- Token Management: Never hardcode API tokens in client-side JavaScript. Use
wp_localize_scriptfor logged-in users, or implement a secure token generation and retrieval mechanism. For public-facing sites, consider if token authentication is truly necessary for all data, or if some endpoints can be made public. - HTTPS: Always use HTTPS to protect tokens and data in transit.
- Token Expiration/Revocation: Implement mechanisms for token expiration and revocation to mitigate risks if a token is compromised.
- Rate Limiting: Protect your API endpoints from abuse by implementing rate limiting, especially if they are publicly accessible.
- Capability Checks: The
permission_callbackshould perform granular capability checks beyond just authentication. Ensure the authenticated user has the necessary permissions to access the requested data. - Input Validation: Sanitize and validate all data received by your API endpoints, even if it’s coming from your own frontend.
- Error Handling: Provide informative but not overly revealing error messages.