How to implement custom Rewrite API custom endpoints endpoints with token authentication in Gutenberg blocks
Leveraging WordPress Rewrite API for Secure Custom Endpoints
WordPress’s Rewrite API offers a powerful mechanism for creating custom endpoints that can be accessed via RESTful URLs. This is particularly useful for building dynamic Gutenberg blocks that require server-side data fetching or manipulation. However, exposing these endpoints without proper authentication is a significant security risk. This guide details how to implement custom endpoints with token-based authentication, ensuring only authorized requests can access your data.
Defining Custom Rewrite Rules and Endpoints
The first step is to register your custom rewrite rules and the corresponding callback functions that will handle the requests. We’ll use the add_rewrite_rule function, hooked into init, to achieve this. For this example, let’s assume we’re creating an endpoint to fetch user-specific data.
add_action( 'init', function() {
// Register a custom rewrite rule for our endpoint
// Example: yoursite.com/api/v1/user-data/(.*)/?$
add_rewrite_rule(
'^api/v1/user-data/(.+?)/?$',
'index.php?custom_endpoint=user_data&user_id=$matches[1]',
'top' // 'top' ensures this rule is checked before default WordPress rules
);
// Add our custom query variable
add_filter( 'query_vars', function( $query_vars ) {
$query_vars[] = 'custom_endpoint';
$query_vars[] = 'user_id';
return $query_vars;
} );
// Flush rewrite rules on plugin activation/deactivation or theme switch
// This is crucial for the new rules to take effect.
// In a real plugin, you'd hook this into activation/deactivation hooks.
// For a theme, it might be on theme switch.
// flush_rewrite_rules(); // Uncomment and run once, or manage via activation hooks.
});
In this code:
add_rewrite_rule: Defines the URL pattern (regex) and the query string it should map to.(.+?)captures the user ID.index.php?custom_endpoint=user_data&user_id=$matches[1]: This is the internal WordPress query that will be triggered. We’re using a custom query variablecustom_endpointto identify our handler anduser_idto pass the captured value.add_filter( 'query_vars', ... ): Registers our custom query variables so WordPress recognizes them.flush_rewrite_rules(): This function is essential. It needs to be called whenever rewrite rules are added or modified. In a production environment, this should be handled by plugin activation/deactivation hooks to avoid performance overhead on every page load.
Handling the Custom Endpoint Request
Now, we need a callback function to process the request when our custom endpoint is hit. We’ll hook into the template_redirect action, which fires before WordPress determines which template to load. This is a good place to intercept requests for our custom endpoints.
add_action( 'template_redirect', function() {
// Get our custom query variables
$custom_endpoint = get_query_var( 'custom_endpoint' );
$user_id = get_query_var( 'user_id' );
// Check if our custom endpoint is being accessed
if ( $custom_endpoint === 'user_data' && ! empty( $user_id ) ) {
// Authentication check will go here
// For now, let's just return some dummy data
$user_data = array(
'id' => absint( $user_id ),
'name' => 'Sample User',
'email' => '[email protected]',
);
// Set the content type to JSON
header( 'Content-Type: application/json' );
// Output the JSON data
echo json_encode( $user_data );
// Stop further WordPress execution
exit;
}
});
This callback:
- Retrieves the values of our custom query variables.
- Checks if the
custom_endpointisuser_dataand if auser_idis present. - (Placeholder) Includes a comment for where the authentication logic will be implemented.
- Sets the
Content-Typeheader toapplication/json. - Encodes and outputs the data as JSON.
- Uses
exit;to prevent WordPress from loading its standard templates, ensuring only our JSON response is sent.
Implementing Token-Based Authentication
For secure API access, we need to implement authentication. A common and effective method is using token-based authentication. This involves generating a unique token for each user or for specific API access, and requiring this token to be sent with each request. We’ll use a custom meta field to store the user’s API token.
Generating and Storing API Tokens
You can generate tokens manually or programmatically. For simplicity, let’s assume you’ll add a custom field to the user profile page in the WordPress admin area to store the token. You’ll need to add this field using the show_user_profile and edit_user_profile actions, and save it using personal_options_update and edit_user_profile_update.
// Add field to user profile page
add_action( 'show_user_profile', 'my_show_extra_profile_fields' );
add_action( 'edit_user_profile', 'my_show_extra_profile_fields' );
function my_show_extra_profile_fields( $user ) {
$api_token = get_user_meta( $user->ID, 'api_token', true );
?>
<?php esc_html_e( 'API Settings', 'textdomain' ); ?>
<table class="form-table">
<tr>
<th><label for="api_token"><?php esc_html_e( 'API Token', 'textdomain' ); ?></label></th>
<td>
<input type="text" name="api_token" id="api_token" value="<?php echo esc_attr( $api_token ); ?>" class="regular-text" readonly="readonly" />
<p class="description"><?php esc_html_e( 'This is your API access token. Keep it secure.', 'textdomain' ); ?></p>
<button type="button" id="generate_new_token"><?php esc_html_e( 'Generate New Token', 'textdomain' ); ?></button>
</td>
</tr>
</table>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#generate_new_token').on('click', function(e) {
e.preventDefault();
// In a real scenario, this would make an AJAX call to generate a new token
// For now, we'll just alert the user.
alert('Token generation logic would go here. You would typically send an AJAX request to a secure endpoint.');
// Example:
// $.post(ajaxurl, { action: 'generate_new_api_token' }, function(response) {
// if (response.success) {
// $('#api_token').val(response.data.token);
// } else {
// alert('Error generating token.');
// }
// });
});
});
</script>
'Unauthorized' ), 403 );
}
$user_id = isset( $_POST['user_id'] ) ? intval( $_POST['user_id'] ) : 0;
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => 'Invalid user ID' ), 400 );
}
// Generate a secure token
$new_token = wp_generate_password( 64, false ); // Generates a 64-character random string
// Save the new token
update_user_meta( $user_id, 'api_token', $new_token );
wp_send_json_success( array( 'token' => $new_token ) );
});
This code snippet:
- Adds an “API Settings” section to the user profile page.
- Displays a read-only input field for the API token.
- Includes a “Generate New Token” button. In a production scenario, this button would trigger an AJAX request to a handler that generates a secure token (e.g., using
wp_generate_password) and saves it viaupdate_user_meta. - The AJAX handler (
wp_ajax_generate_new_api_token) demonstrates how to securely generate and save a new token.
Validating the API Token
Now, let’s integrate the token validation into our template_redirect callback. The token is typically sent in the Authorization header (e.g., Authorization: Bearer YOUR_API_TOKEN) or as a query parameter. Using the Authorization header is generally more secure.
add_action( 'template_redirect', function() {
$custom_endpoint = get_query_var( 'custom_endpoint' );
$user_id = get_query_var( 'user_id' );
if ( $custom_endpoint === 'user_data' && ! empty( $user_id ) ) {
// --- Authentication Check ---
$provided_token = '';
// 1. Check Authorization header
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
$auth_header = $_SERVER['HTTP_AUTHORIZATION'];
// Expecting "Bearer YOUR_TOKEN"
if ( preg_match( '/Bearer\s+(.+)/i', $auth_header, $matches ) ) {
$provided_token = $matches[1];
}
}
// 2. Fallback: Check for token in query parameters (less secure, use with caution)
// elseif ( isset( $_GET['api_token'] ) ) {
// $provided_token = sanitize_text_field( $_GET['api_token'] );
// }
if ( empty( $provided_token ) ) {
header( 'HTTP/1.1 401 Unauthorized' );
echo json_encode( array( 'error' => 'Authentication token is missing.' ) );
exit;
}
// Retrieve the stored token for the requested user
$stored_token = get_user_meta( absint( $user_id ), 'api_token', true );
// Verify the token
if ( empty( $stored_token ) || ! hash_equals( $stored_token, $provided_token ) ) {
header( 'HTTP/1.1 401 Unauthorized' );
echo json_encode( array( 'error' => 'Invalid authentication token.' ) );
exit;
}
// --- End Authentication Check ---
// If authentication passes, proceed to fetch data
$user_data = array(
'id' => absint( $user_id ),
'name' => 'Sample User', // In a real app, fetch this from WP_User object
'email' => '[email protected]', // In a real app, fetch this from WP_User object
);
header( 'Content-Type: application/json' );
echo json_encode( $user_data );
exit;
}
});
Key additions in this section:
- The code now attempts to retrieve the token from the
Authorizationheader, specifically looking for theBearerscheme. - A fallback to checking
$_GET['api_token']is commented out but can be enabled if necessary, though it’s less secure. - If no token is provided, a
401 UnauthorizedHTTP response is sent. - It retrieves the user’s stored API token using
get_user_meta. hash_equals()is used for secure, timing-attack-resistant comparison of the provided token against the stored token.- If the tokens do not match, another
401 Unauthorizedresponse is sent. - Only if authentication is successful does the code proceed to fetch and return the user data.
Integrating with Gutenberg Blocks
To use this custom endpoint from a Gutenberg block, you’ll typically make an AJAX request from the block’s JavaScript. The block editor provides a global wp.apiFetch utility, which is a wrapper around the Fetch API and is aware of WordPress’s REST API. However, for custom endpoints not registered with the REST API, you’ll use the standard Fetch API or a library like Axios.
// Example: Registering a script for your block
add_action( 'enqueue_block_editor_assets', function() {
wp_enqueue_script(
'my-custom-block-editor-script',
get_template_directory_uri() . '/path/to/your/block-editor.js', // Adjust path
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-api-fetch' ),
filemtime( get_template_directory() . '/path/to/your/block-editor.js' ) // Adjust path
);
});
And in your block-editor.js file:
// Assuming you have a block component that needs to fetch data
// For example, within a React component used by your Gutenberg block
import apiFetch from '@wordpress/api-fetch'; // For standard WP REST API endpoints
// For custom endpoints, we'll use the native fetch API
const userIdToFetch = 1; // Example user ID
const apiToken = 'YOUR_USER_API_TOKEN'; // This should be dynamically retrieved or securely managed
async function fetchUserData(userId, token) {
const endpointUrl = `/api/v1/user-data/${userId}/`; // Your custom endpoint URL
try {
const response = await fetch(endpointUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
// Handle errors, e.g., 401 Unauthorized, 404 Not Found
const errorData = await response.json();
console.error('API Error:', response.status, errorData);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('User Data:', data);
return data;
} catch (error) {
console.error('Fetch Error:', error);
// Handle network errors or other exceptions
return null;
}
}
// Example usage within a block component's lifecycle method or event handler:
// componentDidMount() {
// fetchUserData(userIdToFetch, apiToken)
// .then(data => {
// if (data) {
// this.setState({ userData: data });
// }
// });
// }
In the JavaScript:
- We define the URL for our custom endpoint.
- We use the native
fetchAPI. - Crucially, we set the
Authorizationheader with theBearertoken. - Error handling is included to catch non-OK HTTP responses (like 401s) and network errors.
- The response is parsed as JSON.
Important Security Note: Hardcoding API tokens directly in JavaScript is a major security vulnerability. In a real-world application, you would need a secure mechanism to:
- Retrieve the API token for the currently logged-in user on the frontend (e.g., via a nonce-protected AJAX call to a custom endpoint that returns the token, or by embedding it securely in the page data if appropriate for your use case).
- For Gutenberg blocks, consider using
wp.data.select('core').getCurrentUser().idto get the current user ID and then making a secure AJAX call to fetch their token. - Alternatively, if the block is for administrators only, you might embed the token in the script’s data during enqueuing, but this still requires careful consideration of who can access the admin area.
Advanced Considerations and Best Practices
- Rate Limiting: Implement rate limiting on your custom endpoints to prevent abuse and brute-force attacks. This can be done at the server level (e.g., via Nginx) or within your PHP code.
- HTTPS: Always use HTTPS to encrypt communication and protect tokens in transit.
- Token Expiration and Revocation: For enhanced security, consider implementing token expiration and providing a mechanism for users to revoke their tokens. This would involve storing an expiration timestamp with the token and adding a “revoke token” feature.
- Logging: Log authentication attempts (both successful and failed) to monitor for suspicious activity.
- Error Handling: Provide informative but not overly revealing error messages.
- Nonces: For AJAX requests initiated from the WordPress admin or frontend that modify data, always use WordPress nonces to verify the request’s origin and prevent CSRF attacks. While our GET request example doesn’t strictly need a nonce for fetching data, any POST/PUT/DELETE operations would.
- REST API vs. Rewrite API: For standard CRUD operations on WordPress objects, the built-in REST API is often a better choice as it’s well-documented and has built-in security features. The Rewrite API is best suited for highly custom endpoints or when you need complete control over the URL structure and request handling.