Crafting a Secure, User-Scoped Project REST Endpoint in WordPress
When WordPress acts as a headless backend or a service API, extending its default REST capabilities becomes essential. We often need to expose custom data types with highly specific access rules. My focus today is demonstrating how to design and implement a secure custom REST endpoint for user-owned data, ensuring users can only retrieve their own content while administrators retain full access.
Consider a system where users manage "Projects," each a custom post type linked to its author. The core requirement: `User A` sees only their projects, never `User B`'s. First, a minimal Custom Post Type definition for `project`. While `show_in_rest` can expose it via the default API, we're building a custom route for granular control. The `author` support is crucial for tracking ownership.
```php function my_plugin_register_project_cpt() { $labels = ['name' => 'Projects', 'singular_name' => 'Project']; $args = [ 'labels' => $labels, 'public' => true, 'has_archive' => true, 'rewrite' => ['slug' => 'projects'], 'show_in_rest' => true, 'supports' => ['title', 'editor', 'author'], 'capability_type' => 'post', 'map_meta_cap' => true, ]; register_post_type('project', $args); } add_action('init', 'my_plugin_register_project_cpt'); ```
The core of our solution is `register_rest_route`, hooked into `rest_api_init`. Here, we define our endpoint (`/my-plugin/v1/projects`), its allowed methods (`GET` via `WP_REST_Server::READABLE`), the data retrieval logic (`callback`), and critically, the initial security gate (`permission_callback`).
```php function my_plugin_register_project_rest_route() { register_rest_route('my-plugin/v1', '/projects', [ 'methods' => WP_REST_Server::READABLE, 'callback' => 'my_plugin_get_user_projects', 'permission_callback' => 'my_plugin_can_access_projects', 'args' => [], // Define validation for query parameters here if needed ]); } add_action('rest_api_init', 'my_plugin_register_project_rest_route'); ```
The `my_plugin_can_access_projects` function serves as our security pre-check. Its only job is to authorize access to the route itself, not to filter specific data. It verifies the requesting user is logged in and possesses a baseline capability (`read`). If these fail, we return a `WP_Error` with an appropriate HTTP status, preventing the request from proceeding to data retrieval.
```php function my_plugin_can_access_projects(WP_REST_Request $request) { if (!is_user_logged_in()) { return new WP_Error('rest_forbidden', 'You are not logged in.', ['status' => 401]); } if (!current_user_can('read')) { return new WP_Error('rest_forbidden_capability', 'You lack required permissions.', ['status' => 403]); } return true; // Authentication and basic capability checks passed. } ```
This permission callback handles general route access. The actual data scoping—returning only a user's projects versus all projects—occurs within `my_plugin_get_user_projects`. This separation keeps our security gate focused and our data callback responsible for business logic: querying and filtering based on the authenticated user's role and ID.
```php function my_plugin_get_user_projects(WP_REST_Request $request) { $args = ['post_type' => 'project', 'posts_per_page' => -1, 'post_status' => 'publish']; $current_user_id = get_current_user_id();
// Restrict query to current user's projects unless they are an administrator. if (!user_can($current_user_id, 'manage_options')) { $args['author'] = $current_user_id; }
$projects_query = new WP_Query($args); $projects_data = [];
if ($projects_query->have_posts()) { while ($projects_query->have_posts()) { $projects_query->the_post(); $projects_data[] = [ 'id' => get_the_ID(), 'title' => get_the_title(), 'content' => apply_filters('the_content', get_the_content()), 'date' => get_the_date('c'), 'author_id' => get_the_author_meta('ID'), ]; } wp_reset_postdata(); } return new WP_REST_Response($projects_data, 200); } ```
For authentication, logged-in browser users benefit from automatic cookie transmission, enabling `is_user_logged_in()` and `get_current_user_id()`. For external applications, leverage Application Passwords or OAuth for secure, token-based authentication against these custom endpoints. This pattern empowers you to build secure, decoupled, and highly custom experiences with WordPress as a robust backend.