How to implement custom REST API Controllers endpoints with token authentication in Gutenberg blocks
Registering Custom REST API Endpoints in WordPress
To extend WordPress’s REST API with custom functionality, particularly for integration with Gutenberg blocks, we need to register new routes and endpoints. This involves leveraging the `register_rest_route` function, which allows us to define the namespace, route, and the callback function that will handle requests to that endpoint. For e-commerce scenarios, these endpoints might expose product data, process orders, or manage user-specific information.
The core of this process lies in hooking into the `rest_api_init` action. This action fires when the REST API is being initialized, providing the perfect opportunity to register our custom routes.
Implementing a Basic Endpoint with a Callback
Let’s start by creating a simple endpoint that returns a JSON response. We’ll define a namespace, a route, and a callback function. The callback function receives a `WP_REST_Request` object, which contains all the information about the incoming request.
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/products', array(
'methods' => WP_REST_Server::READABLE, // Equivalent to GET
'callback' => 'myplugin_get_products_callback',
'permission_callback' => '__return_true', // For now, allow all
) );
} );
function myplugin_get_products_callback( WP_REST_Request $request ) {
// In a real scenario, fetch products from the database or an external service.
$products = array(
array(
'id' => 1,
'name' => 'Awesome Gadget',
'price' => 99.99,
),
array(
'id' => 2,
'name' => 'Super Widget',
'price' => 49.50,
),
);
return new WP_REST_Response( $products, 200 );
}
In this example:
myplugin/v1is our custom namespace and version. It’s crucial to use a unique namespace to avoid conflicts with other plugins or WordPress core./productsis the specific route for this endpoint.WP_REST_Server::READABLEspecifies that this endpoint responds to GET requests. Other common methods includeWP_REST_Server::CREATABLE(POST),WP_REST_Server::EDITABLE(POST, PUT, PATCH), andWP_REST_Server::DELETABLE(DELETE).myplugin_get_products_callbackis the PHP function that will be executed when a request hits this endpoint.__return_trueis a placeholder for the permission callback. We’ll address authentication and authorization in detail later.
Implementing Token-Based Authentication
For Gutenberg blocks, especially those interacting with sensitive data or performing actions on behalf of a user, robust authentication is paramount. WordPress’s REST API supports various authentication methods, but for custom integrations, token-based authentication is often preferred. This typically involves generating a unique token for a user or an application and requiring it in subsequent requests.
We’ll implement a simple token authentication mechanism. This involves:
- Generating and storing tokens (e.g., as user meta).
- Validating the token on incoming requests.
- Associating the authenticated user with the request.
Generating and Storing Tokens
Tokens can be generated when a user logs in, or through a dedicated API endpoint for application integration. For user-specific tokens, storing them as user meta is a common practice. We’ll use `wp_generate_password` for token generation and `update_user_meta` to store it.
// Function to generate and store a token for a user
function myplugin_generate_user_token( $user_id ) {
if ( ! $user_id ) {
return false;
}
// Generate a secure, unique token
$token = wp_generate_password( 64, false, false ); // 64 characters, no special chars, no numbers
// Store the token as user meta
update_user_meta( $user_id, 'myplugin_api_token', $token );
return $token;
}
// Example: Generate token on user profile update (for demonstration)
add_action( 'show_user_profile', 'myplugin_display_token_field' );
add_action( 'edit_user_profile', 'myplugin_display_token_field' );
function myplugin_display_token_field( $user ) {
$token = get_user_meta( $user->ID, 'myplugin_api_token', true );
?>
<h3></h3>
<table class="form-table">
<tr>
<th scope="row"></th>
<td>
<input type="text" readonly="readonly" value="" class="regular-text" />
<p class="description"></p>
<button type="button" id="myplugin-generate-token" class="button"></button>
</td>
</tr>
</table>
<script>
jQuery(document).ready(function($) {
$('#myplugin-generate-token').on('click', function() {
var data = {
'action': 'myplugin_regenerate_token',
'user_id': ,
'_ajax_nonce': ''
};
$.post(ajaxurl, data, function(response) {
if (response.success) {
$('input[type="text"]').val(response.data.token);
alert('');
} else {
alert('');
}
});
});
});
</script>
This code adds a field to the user profile page displaying the current token and a button to generate a new one. The generation process uses AJAX to avoid page reloads.
Validating Tokens in REST API Endpoints
Now, we need to modify our endpoint registration to include a robust permission callback that checks for a valid token. The token is typically passed in the `Authorization` header as a Bearer token, or as a query parameter.
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/products', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'myplugin_get_products_callback',
'permission_callback' => 'myplugin_check_api_token',
) );
// Example: Endpoint to retrieve the current user's token
register_rest_route( 'myplugin/v1', '/me/token', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'myplugin_get_current_user_token',
'permission_callback' => 'myplugin_check_api_token', // Requires authentication to get token
) );
} );
function myplugin_check_api_token( WP_REST_Request $request ) {
$token = null;
// 1. Check for token in Authorization header
$auth_header = $request->get_header( 'Authorization' );
if ( ! empty( $auth_header ) ) {
if ( preg_match( '/Bearer\s(\S+)/', $auth_header, $matches ) ) {
$token = $matches[1];
}
}
// 2. If not in header, check for token in query parameters (less secure, use with caution)
if ( empty( $token ) ) {
$token = $request->get_param( 'api_token' );
}
if ( empty( $token ) ) {
return new WP_Error( 'rest_not_logged_in', 'No authentication token provided.', array( 'status' => 401 ) );
}
// Find user by token
$user_id = null;
$users = get_users( array(
'meta_key' => 'myplugin_api_token',
'meta_value' => $token,
) );
if ( ! empty( $users ) ) {
$user_id = $users[0]->ID;
}
if ( ! $user_id ) {
return new WP_Error( 'rest_invalid_token', 'Invalid authentication token.', array( 'status' => 401 ) );
}
// Set the authenticated user for the request
wp_set_current_user( $user_id );
// Return true if authentication is successful
return true;
}
function myplugin_get_products_callback( WP_REST_Request $request ) {
// The permission_callback has already authenticated the user.
// We can now access the current user if needed.
$current_user = wp_get_current_user();
// In a real scenario, fetch products.
$products = array(
array(
'id' => 1,
'name' => 'Awesome Gadget',
'price' => 99.99,
'added_by' => $current_user->display_name, // Example of using authenticated user
),
// ... more products
);
return new WP_REST_Response( $products, 200 );
}
function myplugin_get_current_user_token( WP_REST_Request $request ) {
$current_user = wp_get_current_user();
if ( ! $current_user->exists() ) {
return new WP_Error( 'rest_not_authenticated', 'User not authenticated.', array( 'status' => 401 ) );
}
$token = get_user_meta( $current_user->ID, 'myplugin_api_token', true );
return new WP_REST_Response( array( 'token' => $token ), 200 );
}
The `myplugin_check_api_token` function:
- First attempts to retrieve the token from the
Authorization: Bearer <token>header. - If not found, it falls back to checking for an
api_tokenquery parameter. - It then queries the user database for a user whose meta value for
myplugin_api_tokenmatches the provided token. - If a user is found, `wp_set_current_user` is called to make that user the current user for the request, allowing subsequent callbacks to access user information via `wp_get_current_user()`.
- If no token is provided or the token is invalid, it returns a
WP_Errorwith a 401 status code.
Integrating with Gutenberg Blocks
For Gutenberg blocks, the REST API endpoints are typically accessed using JavaScript. The block's JavaScript file will make `fetch` requests to your custom endpoints. Authentication is handled by including the API token in the request headers.
First, ensure your block's JavaScript is enqueued correctly, and that it has access to the API URL and potentially the user's token (if it's a user-specific block). You can pass data from PHP to JavaScript using `wp_localize_script`.
// In your plugin's main PHP file or block registration file
function myplugin_enqueue_block_scripts() {
// Enqueue your block's JavaScript file
wp_enqueue_script(
'myplugin-block-editor-js',
plugins_url( 'build/index.js', __FILE__ ), // Path to your compiled JS
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
// Localize script to pass data to JavaScript
wp_localize_script( 'myplugin-block-editor-js', 'mypluginData', array(
'api_url' => esc_url_raw( rest_url( 'myplugin/v1/' ) ),
// You might fetch the user's token and pass it here, or have the JS fetch it.
// Be cautious about exposing tokens directly if not necessary.
// For user-specific blocks, consider fetching the token via a dedicated endpoint.
) );
}
add_action( 'enqueue_block_editor_assets', 'myplugin_enqueue_block_scripts' );
JavaScript Example for Fetching Data
Here's a simplified JavaScript example demonstrating how a Gutenberg block could fetch data from your custom endpoint. This assumes you have a way to get the user's token (e.g., by fetching it from the `/me/token` endpoint or if it's available globally).
// Assuming mypluginData.api_url is available from wp_localize_script
// And assuming you have a function to get the user's token, e.g., getUserToken()
const { registerBlockType } = wp.blocks;
const { Component } = wp.element;
const { apiFetch } = wp; // WordPress's built-in fetch wrapper
// Placeholder for getting the token. In a real app, this would involve
// fetching it from user meta via a dedicated REST endpoint or other secure means.
async function getUserToken() {
// Example: Fetch token from a dedicated endpoint
try {
const response = await apiFetch({
path: 'myplugin/v1/me/token',
method: 'GET',
});
return response.token;
} catch (error) {
console.error('Error fetching user token:', error);
return null;
}
}
registerBlockType( 'myplugin/product-list', {
title: 'Product List',
icon: 'list-view',
category: 'widgets',
edit: class extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
isLoading: true,
error: null,
token: null,
};
}
async componentDidMount() {
const token = await getUserToken();
if (!token) {
this.setState({ error: 'Authentication token not found.', isLoading: false });
return;
}
this.setState({ token: token });
this.fetchProducts(token);
}
async fetchProducts(token) {
try {
const response = await fetch(mypluginData.api_url + 'products', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const products = await response.json();
this.setState({ products: products, isLoading: false });
} catch (error) {
console.error('Error fetching products:', error);
this.setState({ error: error.message, isLoading: false });
}
}
render() {
const { products, isLoading, error } = this.state;
if (isLoading) {
return <div>Loading products...</div>;
}
if (error) {
return <div style={{ color: 'red' }}>Error: {error}</div>;
}
if (products.length === 0) {
return <div>No products found.</div>;
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
);
}
},
save: () => {
// The save function for dynamic blocks typically returns null
// as the content is rendered server-side or fetched dynamically.
return null;
},
} );
In this JavaScript:
- We use `apiFetch` (WordPress's wrapper around `fetch`) to get the user's token from our custom endpoint. This is a more secure way than passing it directly from PHP if the token isn't strictly necessary for the block's initial rendering.
- The `fetchProducts` function makes a `GET` request to our `/products` endpoint, including the `Authorization` header with the Bearer token.
- Error handling is included for network issues or API-level errors (e.g., invalid token, unauthorized access).
- The `save` function for this block would typically return `null` if it's a dynamic block that renders its content on the server or fetches it client-side.
Security Considerations and Best Practices
When implementing custom API endpoints and token authentication, security must be a top priority:
- Token Generation: Always use cryptographically secure methods for token generation (e.g., `wp_generate_password` with appropriate parameters).
- Token Storage: Store tokens securely. User meta is generally acceptable for user-specific tokens, but ensure the database is protected. Avoid storing tokens in client-side JavaScript directly unless absolutely necessary and with strict expiration policies.
- HTTPS: Always use HTTPS to protect tokens in transit.
- Token Revocation: Provide a mechanism for users to revoke their API tokens (e.g., a "reset token" button on their profile).
- Rate Limiting: Implement rate limiting on your API endpoints to prevent abuse and brute-force attacks.
- Input Validation: Sanitize and validate all data received by your API endpoints, even if it's authenticated.
- Principle of Least Privilege: The `permission_callback` should grant access only to users who genuinely need it for the specific endpoint. Avoid `__return_true` in production for sensitive operations.
- Error Messages: Be careful not to reveal too much information in error messages that could aid attackers.
By following these guidelines, you can build secure and robust custom REST API endpoints for your WordPress e-commerce site, enabling powerful integrations with Gutenberg blocks and other applications.