Alright, guess I figured it all out:
Has WP Core been updated to do JSON Schema – based request argument validation automatically now?
It seems that that’s the case, yes. If you provide a proper JSON schema, as shown in the example, when using register_rest_route
, payload validation is executed automatically, without the provision of a validate_callback
for your args
.
Does payload validation before calling your permission_callback
?
Apparently that’s the case too. Consequently, if you do not rely on wordpress cookies for your custom WP REST API calls, unauthorized clients are actually able to get validation feedback, which imho is not always ideal.
…if WordPress does any kind of REST authentication by default?
It seems that this is not the case if you do not rely on a client being logged in, as wordpress seems to use cookies for authentication, and their built-in authorization using WP nonces apparently does not work without cookies. Meaning that, if you do not want to rely on wordpress cookies, maybe if you have your own login / user / session logic, you must also implement your own authentication mechanism. That’s my assumption, as no authentication step seems to happen by default via REST requests, if you do not use wordpress cookies.
I thus searched for a way to implement an endpoint-specific authentication, and figured that using the rest_pre_dispatch
filter hook may be the best option to do so. From the docs linked above, what the filter does is:
Allow hijacking the request before dispatching by returning a
non-empty. The returned value will be used to serve the request
instead.
So you can do something like this in your main plugin file, for example:
/**
* Authentication
*
* This section sets up the authentication for the different endpoints.
*/
add_filter(
'rest_pre_dispatch',
function (
$result,
WP_REST_Server $server,
WP_REST_Request $request
) {
// route value is full endpoint path
$requested_route = $request->get_route();
// Assuming you want to authenticate requests of a given namespace
if ( str_starts_with($requested_route, '/yourapinamespace/v1') ) {
// May change the number here, 20 matches the position after v1
$resource = substr($requested_route,20);
$request_method = $request->get_method();
switch ( $resource ) {
case '/purchases':
return match ($request_method) {
'POST' => Authenticate::for_purchase($request),
// Do not allow unauthenticated requests
default => new WP_Error(
401,
esc_html__('Authentication failed!','my-txt-domain'),
[
'status' => 401
]
)
};
default:
return null;
}
}
return null;
},
10,
3
);
If everything’s fine, return null
in your callback, and the request will get through in the usual way. Otherwise, the request will get interrupted immediately, and the WP_Error
instance you return will be used for the response back to the client.
I prefer this solution, as it acts immediately, and avoids any unnecessary roundtrips in case of unauthenticated requests. Remember though that payload validation has not occurred at the stage of the rest_pre_dispatch filter
. So make sure you won’t treat your request arguments as if they were validated within your authentication callbacks.
Finally, remember to also implement your authorization after this, and automatically execute it after successful authentication. I see two options here:
- You can still do so by providing the according callback as a
permission_callback
when registering your endpoint viaregister_rest_route
. Using apermission_callback
however means that your endpoint will execute an authorization check after the payload validation; hence authenticated but unauthorized clients may get information about payload validation problems. - The second solution would be to implement your own nonce check by calling
check_ajax_referer()
or similar within yourAuthenticate::for_purchase($request)
, and simply set thepermission_callback
when registering your endpoint to__return_true
.