0

Suppose you register the following endpoint in your application on example.org:

register_rest_route(
    'sample/v1',
    '/test',
    [
        [
            'methods'             => 'POST',
            'permission_callback' => [
                TestsController::class,
                'permission_check'
            ],
            'callback'            => [
                TestsController::class,
                'run'
            ],
            'args'                => [
                'id' => [
                    'type'        => 'integer',
                    'description' => 'Identifier of the request',
                    'required'    => true
                ]
            ]
        ]
    ]
);

With the contents of TestController being:

final class TestsController
{
    
    /**
     * @param  WP_REST_Request  $request
     *
     * @return WP_REST_Response
     */
    public static function run( WP_REST_Request $request ): WP_REST_Response
    {
        
        // This is just a test, nothing is done here
        
        return new WP_REST_Response(
            [
                'success' => true,
                'message' => 'All good!'
            ],
            200
        );
        
    }
    
    /**
     * @param  WP_REST_Request  $request
     *
     * @return bool
     */
    public static function permission_check( WP_REST_Request $request ): bool
    {
        return true;
    }
    
}

A few months ago, I've asked how you can validate requests incoming to your custom WordPress REST API endpoints via the JSON Schema you specified when doing register_rest_route. Back then, the answer was by specifying the validate_callback parameter for every single request argument, specified in args. When you however do the above-mentioned and send a request with no or an invalid payload, validation automatically takes place, and you get an according 400 error back. Has WP Core been updated to do JSON Schema - based request argument validation automatically now?

Regarding the permission_callback argument of the endpoint, the docs mention that:

Note that the permission callback also receives the Request object as the first parameter, so you can do checks based on request arguments if you need to.

I suppose that this means that WordPress does payload validation before calling your permission_callback?

Finally, suppose you have a website which uses a custom login system, hence which does not rely on WordPress cookies for the Frontend. This would then mean that WordPress would provide information about why a payload is bad to unauthenticated clients, which does not seem preferential to me.

Further down in the above-linked docs, and I suppose exactly due to this problem; I found the following note:

Once you register a permission_callback, you will need to authenticate your requests (for example by including a nonce parameter) or you will receive a rest_forbidden error.

This however does not seem to be true. Try registering my test endpoint above, and fire a request from any HTTP client, for example:

POST https://example.org/wp-json/sample/v1/test
Content-Type: application/json

{
"id":"hello"
}

You will not get any rest_forbidden error, but a rest_missing_callback_param error with the details explaining why your payload is invalid. This by the way again happens automatically now, without the provision of validate_callback to your id argument. Hence I wonder if WordPress does any kind of REST authentication by default?

Due to all of this, my final question is: Does WordPress provide a hook or a filter which allows you to do an authentication which relies on your own cookie / token logic, before the payload gets validated / before you step into authorization? Such that these steps only get executed if your authentication is passed? Something in the sense of:

register_rest_route(
    'sample/v1',
    '/test',
    [
        [
            'methods'             => 'POST',
            // This would then get executed before anything else
            'authentication_callback' => [
                TestsController::class,
                'authentication_check'
            ],
            'permission_callback' => [
                TestsController::class,
                'permission_check'
            ],
            'callback'            => [
                TestsController::class,
                'run'
            ],
            'args'                => [
                'id' => [
                    'type'        => 'integer',
                    'description' => 'Identifier of the request',
                    'required'    => true
                ]
            ]
        ]
    ]
);

You may also imagine what I need as an implementation of the documented rest_authentication_errors filter, but on a per-endpoint specific level.

1 Answer 1

0

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:

  1. You can still do so by providing the according callback as a permission_callback when registering your endpoint via register_rest_route. Using a permission_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.
  2. The second solution would be to implement your own nonce check by calling check_ajax_referer() or similar within your Authenticate::for_purchase($request), and simply set the permission_callback when registering your endpoint to __return_true.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.