1

I have created a number of RESTful endpoints via a plugin. To date I have only accessed them from the host site. However I would like to access these endpoints from a sub-domaine(2ed Wordpress installation). An application password appears to be a good way to set up authentication for accessing these endpoints in my application. However I can not find any documentation as to how to modify my 'permission_callback' to recognize application passwords.

8
  • There's been a major misunderstanding of what permission_callback does here. The permission_callback callback is not meant to authenticate users, it's meant as a check that an already authenticated user has access to that endpoint, e.g. via roles and capabilities. WordPress itself implements the login authentication, be that via cookie + nonce, or application passwords, not the endpoint
    – Tom J Nowell
    Commented Feb 21, 2023 at 10:30
  • No I understand what permission_callback is for, The confusion I have not been able to resolve is how WordPress site 1( the one with the RESTfull extensions,) knows anything about users on Workpress site 2 a client of the RESTfull endpoints provided by Wordpress site 1. The user is authenticated on site 2, but site 1 knows nothing about that user.
    – dsj
    Commented Feb 21, 2023 at 16:28
  • it isn't, you can't use a user on site 1 to login to site 2. To login to a WP site you need to login to a user on that specific site. You can do that via basic auth with a username and password of a user on that site, or with an application password set up by a user on that site, but it's specific to that site. If you are on site 2, and want to make an authenticated request to site 1, you need the credentials of a user on site 1. It's the same as if you tried to talk to the google or FB APIs, you don't use your apps user details to login to those, you use FB details, or Google details
    – Tom J Nowell
    Commented Feb 21, 2023 at 20:54
  • what that has to do with permission_callback I do not know, permission_callback is equivalent to current_user_can, not is_user_logged_in, and framing it that way in your mind will only create more confusion. To clarify, site 1 has no idea about the users on Site 2, and vice versa, and site 1 has no idea if the user is logged into site 2 or not, much the same way my site has no idea if you are logged into your site. Also, site 1 makes no attempt to check if the user of site 2 is logged into site 2, that would make no sense. Site 1 is only interested in the users of & requests to site 1
    – Tom J Nowell
    Commented Feb 21, 2023 at 20:55
  • so if you want to make a request to site 1 that require authentication, you need to authenticate with site 1, and user an application password that was created on site 1, with a user that exists on site 1. This is true regardless of where the request comes from, be that site 2, mobile application 3, or site 1 itself.
    – Tom J Nowell
    Commented Feb 21, 2023 at 20:59

3 Answers 3

1

If you want to use application passwords to authenticate requests to your RESTful endpoints, you can modify your permission_callback function to check for the presence and validity of an application password instead of a user's credentials.

To do this, you can use the wp_check_application_passwords function, which was introduced in WordPress 5.6. This function takes an application password and a user ID (or a username) and returns a WP_Error object if the password is invalid or expired, or an array with information about the password if it's valid. Here's an example of how you could modify your permission_callback function to use application passwords:

function my_rest_permission_callback( $request ) {
    $app_password = $request->get_header( 'X-WP-Application-Password' );

    if ( empty( $app_password ) ) {
        return new WP_Error( 'rest_forbidden', __( 'Authentication required.' ), array( 'status' => 401 ) );
    }

    $user_id = get_current_user_id(); // or get the user ID from the request data

    $result = wp_check_application_passwords( $user_id, $app_password );

    if ( is_wp_error( $result ) ) {
        return new WP_Error( 'rest_forbidden', $result->get_error_message(), array( 'status' => 403 ) );
    }

    // Here you can do additional checks or data processing based on the password information
    // ...

    return true; // Access granted
}

In this example, the permission_callback function retrieves the X-WP-Application-Password header from the request, which should contain the application password provided by the client. If the header is empty, the function returns a 401 Unauthorized error.

The function then calls wp_check_application_passwords with the current user ID and the application password, and checks if the result is an error. If so, the function returns a 403 Forbidden error with the message from the WP_Error object. Otherwise, the function assumes that the password is valid and returns true to allow access to the endpoint.

You can customize this code to fit your specific needs, such as checking additional information in the password result or handling errors differently. Note that you need to create and manage application passwords in the WordPress administration area, under the user profile of the user who will be making the requests.

1
  • This is not how application passwords are meant to work, and bad practice
    – Tom J Nowell
    Commented Feb 21, 2023 at 10:18
1

It would use is_user_logged_in(). It's WordPress' job to authenticate the user not yours. permission_callback is there to test if that user has access, e.g. can an editor access this endpoint:

The permissions callback is run after remote authentication, which sets the current user. This means you can use current_user_can to check if the user that has been authenticated has the appropriate capability for the action, or any other check based on current user ID. Where possible, you should always use current_user_can; instead of checking if the user is logged in (authentication), check whether they can perform the action (authorization).

https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/#permissions-callback

If you are testing WP user logins, application passwords, etc in your permission_callback then this is incorrect, and not how the REST API works.

Authenticated requests to the REST API are either:

  • basic auth with application passwords
  • cookie sessions + REST API nonce

WordPress will log the user in and create sessions on its own, it does all of that for you. Your code only needs to know is_user_logged_in(), and you don't need to do anything special for 3rd party remote clients.

3
  • And how does the endpoint know they are logged in when they are coming from a remote website?
    – dsj
    Commented Feb 20, 2023 at 20:27
  • it doesn't, it's not the job of the endpoint to figure that out, WordPress handles user sessions and authenticated endpoints don't implement the authentication by hand themselves. They just check that you're logged in. How they are logged in is none of your business, all you need concern yourself is the return value of is_user_logged_in()
    – Tom J Nowell
    Commented Feb 21, 2023 at 10:14
  • note that if you are writing code in your REST API endpoints checking user passwords, creating user sessions, etc, that is incorrect and not how REST API endpoints work. WordPress is meant to do all of that for you, and that's not what that callback is there for. This is why the other answer is incorrect, and bad practice
    – Tom J Nowell
    Commented Feb 21, 2023 at 10:18
1

Building on @Tom J Newells answer, he's correct and you ideally shouldn't try to create your own user authentication process for 3 good reasons I can think of;

1: WordPress has already checked to see if the user is logged in. 2: WordPress may change the login criteria. 3: There's no point trying to reinvent the wheel.

Consider the following code

//Register our routes
public function register_api_routes(){

    register_rest_route(
        $this->plugin_name.'/v1', 
        '/book', 
        [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [ $this, 'get_book' ],
                'permission_callback' => [$this, 'get_book_permissions_check']
            ],
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [ $this, 'create_book' ],
                'permission_callback' => [$this, 'create_book_permissions_check']
            ],
        ]
    );  

}

public function get_book_permissions_check( $request ) {

    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_cannot_read_book',
            __( 'Sorry, you are not allowed to read books.' ),
            array( 'status' => rest_authorization_required_code() )
        );
    }

    return true;
}

public function create_book_permissions_check( $request ) {

    $book_type = $this->get_book_type_object( $this->book_type );

    if ( ! empty( $request['author'] ) || get_current_user_id() !== $request['author'] && ! current_user_can( $book_type->cap->edit_others_posts ) ) {
        return new WP_Error(
            'rest_cannot_read_book',
            __( 'Sorry, you are not allowed to create books.' ),
            array( 'status' => rest_authorization_required_code() )
        );
    }

    return true;
}

public function get_book(WP_REST_Request $request){
    //get_book() validation has already checked if the user is logged in
    //so at this point assume the user can look up books
    $user = wp_get_current_user();

    $widgetRef= $request->get_param('book_reference');

    $lookup_result = $book->book_get();

    return new WP_REST_Response( $lookup_result, 200);
}

public function create_book(WP_REST_Request $request){
    // create_book() validation has already checked if the user is 
    // capable of performing defined actions so at this point 
    // (Have to be logged in to be capable)
    $this->user = wp_get_current_user();

    $create_result = $book->book_put();

    return new WP_REST_Response( $create_result, 200);
}

WordPress handles Application Password authentication before processing of callbacks.

WordPress will assume all of your custom endpoints are public and unless you either check for is_user_logged_in() or current_user_can() then WordPress will assumes you allow it.

So you send authentication with every request where needed.

curl --user "USERNAME:PASSWORD" https://HOSTNAME/wp-json/wp/v1/book

Check wp-includes/rest-api/ for more examples of how endpoints are handled.

Updated: I've been looking into this more. My opinion on WP API Credentials so far is that it's still very much in it's infancy. There's something I don't like about it. @Tom J Newells is still correct in that you should not be authenticating users, I beleive you should be authenticating application passwords against known applications and application passwords.

As an example, you can create a named application and then an application password for it, but there's no way to assign a specific API Password to a API service in cases where you have more than one API service. Any user with an Application Password can access any API service running on the instance of WordPress.

Let's say for instance you create an Application and Application Password under one user that you intend to use for one API service and then you create another Application and Application Password on another user that you want to user for another API service. You don't want either set of credentials to be able to access the other API service. Currently there isn't anything within WordPress to support this automatically. Any authenticated API user can access any API service under a different user.

From here https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/#permissions-callback it's true that the permissions callback is run after remote authentication (which sets the current user). You're then advised to use current_user_can() to check if the user has appropriate capability to perform an action. So if both users have been set as administrator and both return true for current_user_can('edit_other_posts') then either user could perform full actions within either API services.

If you wanted to tie a particular user/application password to a particular service then use below in your permission_callback

wp_authenticate_application_password( WP_User|WP_Error|null $input_user, string $username, string $password ): WP_User|WP_Error|null

In use, it would be something like this

$current_user = wp_get_current_user();
wp_authenticate_application_password( $current_user, $current_user->user_login, 'Your Application Password' );

On my plugin activation I create an admin user wp_insert_user(), which gets an Application Name and Application Password created against it WP_Application_Passwords::create_new_application_password(). This is then stored in my plugin DB settings. I replace 'Your Application Password' with a variable containing the password.

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.