WordPress.org

Make WordPress Core

Changeset 50065


Ignore:
Timestamp:
01/29/2021 12:05:20 AM (8 months ago)
Author:
TimothyBlynJacobs
Message:

App Passwords: Introduce introspection endpoint.

This introduces a new endpoint, wp/v2/users/me/application-passwords/introspect, that will return details about the App Password being used to authenticate the current request. This allows for an application to disambiguate between multiple installations of their application which would all share the same app_id.

Props xkon, peterwilsoncc, TimothyBlynJacobs.
Fixes #52275.

Location:
trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/default-filters.php

    r49992 r50065  
    281281add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' );
    282282add_action( 'application_password_failed_authentication', 'rest_application_password_collect_status' );
    283 add_action( 'application_password_did_authenticate', 'rest_application_password_collect_status' );
     283add_action( 'application_password_did_authenticate', 'rest_application_password_collect_status', 10, 2 );
    284284add_filter( 'rest_authentication_errors', 'rest_application_password_check_errors', 90 );
    285285add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
  • trunk/src/wp-includes/rest-api.php

    r50060 r50065  
    10491049 *
    10501050 * @since 5.6.0
     1051 * @since 5.7.0 Added the `$app_password` parameter.
    10511052 *
    10521053 * @global WP_User|WP_Error|null $wp_rest_application_password_status
     1054 * @global string|null $wp_rest_application_password_uuid
    10531055 *
    10541056 * @param WP_Error $user_or_error The authenticated user or error instance.
    1055  */
    1056 function rest_application_password_collect_status( $user_or_error ) {
    1057     global $wp_rest_application_password_status;
     1057 * @param array    $app_password  The Application Password used to authenticate.
     1058 */
     1059function rest_application_password_collect_status( $user_or_error, $app_password = array() ) {
     1060    global $wp_rest_application_password_status, $wp_rest_application_password_uuid;
    10581061
    10591062    $wp_rest_application_password_status = $user_or_error;
     1063
     1064    if ( empty( $app_password['uuid'] ) ) {
     1065        $wp_rest_application_password_uuid = null;
     1066    } else {
     1067        $wp_rest_application_password_uuid = $app_password['uuid'];
     1068    }
     1069}
     1070
     1071/**
     1072 * Gets the Application Password used for authenticating the request.
     1073 *
     1074 * @since 5.7.0
     1075 *
     1076 * @global string|null $wp_rest_application_password_uuid
     1077 *
     1078 * @return string|null The App Password UUID, or null if Application Passwords was not used.
     1079 */
     1080function rest_get_authenticated_app_password() {
     1081    global $wp_rest_application_password_uuid;
     1082
     1083    return $wp_rest_application_password_uuid;
    10601084}
    10611085
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php

    r50030 r50065  
    5353                    'callback'            => array( $this, 'delete_items' ),
    5454                    'permission_callback' => array( $this, 'delete_items_permissions_check' ),
     55                ),
     56                'schema' => array( $this, 'get_public_item_schema' ),
     57            )
     58        );
     59
     60        register_rest_route(
     61            $this->namespace,
     62            '/' . $this->rest_base . '/introspect',
     63            array(
     64                array(
     65                    'methods'             => WP_REST_Server::READABLE,
     66                    'callback'            => array( $this, 'get_current_item' ),
     67                    'permission_callback' => array( $this, 'get_current_item_permissions_check' ),
     68                    'args'                => array(
     69                        'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     70                    ),
    5571                ),
    5672                'schema' => array( $this, 'get_public_item_schema' ),
     
    375391
    376392    /**
     393     * Checks if a given request has access to get the currently used application password.
     394     *
     395     * @since 5.7.0
     396     *
     397     * @param WP_REST_Request $request Full details about the request.
     398     * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
     399     */
     400    public function get_current_item_permissions_check( $request ) {
     401        $user = $this->get_user( $request );
     402
     403        if ( is_wp_error( $user ) ) {
     404            return $user;
     405        }
     406
     407        if ( get_current_user_id() !== $user->ID ) {
     408            return new WP_Error(
     409                'rest_cannot_introspect_app_password_for_non_authenticated_user',
     410                __( 'The authenticated Application Password can only be introspected for the current user.' ),
     411                array( 'status' => rest_authorization_required_code() )
     412            );
     413        }
     414
     415        return true;
     416    }
     417
     418    /**
     419     * Retrieves the application password being currently used for authentication.
     420     *
     421     * @since 5.7.0
     422     *
     423     * @param WP_REST_Request $request Full details about the request.
     424     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     425     */
     426    public function get_current_item( $request ) {
     427        $user = $this->get_user( $request );
     428
     429        if ( is_wp_error( $user ) ) {
     430            return $user;
     431        }
     432
     433        $uuid = rest_get_authenticated_app_password();
     434
     435        if ( ! $uuid ) {
     436            return new WP_Error(
     437                'rest_no_authenticated_app_password',
     438                __( 'Cannot introspect Application Password.' ),
     439                array( 'status' => 404 )
     440            );
     441        }
     442
     443        $password = WP_Application_Passwords::get_user_application_password( $user->ID, $uuid );
     444
     445        if ( ! $password ) {
     446            return new WP_Error(
     447                'rest_application_password_not_found',
     448                __( 'Application password not found.' ),
     449                array( 'status' => 500 )
     450            );
     451        }
     452
     453        return $this->prepare_item_for_response( $password, $request );
     454    }
     455
     456    /**
    377457     * Performs a permissions check for the request.
    378458     *
  • trunk/tests/phpunit/tests/auth.php

    r49919 r50065  
    3939        wp_set_current_user( self::$user_id );
    4040        update_site_option( 'using_application_passwords', 1 );
     41
     42        unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] );
    4143    }
    4244
     
    4547
    4648        // Cleanup all the global state.
    47         unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'] );
     49        unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] );
    4850    }
    4951
     
    443445
    444446        // Create a new app-only password.
    445         list( $user_app_password ) = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'phpunit' ) );
     447        list( $user_app_password, $item ) = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'phpunit' ) );
    446448
    447449        // Fake a REST API request.
     
    453455        $_SERVER['PHP_AUTH_PW']   = 'http_auth_pass';
    454456
    455         $this->assertSame(
    456             null,
     457        $this->assertNull(
    457458            wp_validate_application_password( null ),
    458459            'Regular user account password should not be allowed for API authentication'
    459460        );
     461        $this->assertNull( rest_get_authenticated_app_password() );
    460462
    461463        // Not try with an App password instead.
     
    467469            'Application passwords should be allowed for API authentication'
    468470        );
     471        $this->assertEquals( $item['uuid'], rest_get_authenticated_app_password() );
    469472    }
    470473
  • trunk/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php

    r50030 r50065  
    6868
    6969        add_filter( 'wp_is_application_passwords_available', '__return_true' );
     70    }
     71
     72    public function tearDown() {
     73        parent::tearDown();
     74        unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] );
    7075    }
    7176
     
    878883        $this->assertCount( 7, $properties );
    879884    }
     885
     886    /**
     887     * @ticket 52275
     888     */
     889    public function test_introspect_item() {
     890        $password = $this->setup_app_password_authenticated_request();
     891        $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
     892        $this->assertNotWPError( $response->as_error() );
     893
     894        $this->assertEquals( $password['uuid'], $response->get_data()['uuid'] );
     895    }
     896
     897    /**
     898     * @ticket 52275
     899     */
     900    public function test_introspect_item_specific_user() {
     901        $password = $this->setup_app_password_authenticated_request();
     902        $response = rest_do_request( '/wp/v2/users/' . self::$admin . '/application-passwords/introspect' );
     903
     904        $this->assertEquals( $password['uuid'], $response->get_data()['uuid'] );
     905    }
     906
     907    /**
     908     * @ticket 52275
     909     */
     910    public function test_introspect_item_logged_out() {
     911        $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
     912        $this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
     913    }
     914
     915    /**
     916     * @ticket 52275
     917     */
     918    public function test_introspect_item_wrong_user() {
     919        $this->setup_app_password_authenticated_request();
     920        $response = rest_do_request( '/wp/v2/users/' . self::$subscriber_id . '/application-passwords/introspect' );
     921        $this->assertErrorResponse( 'rest_cannot_introspect_app_password_for_non_authenticated_user', $response, 403 );
     922    }
     923
     924    /**
     925     * @ticket 52275
     926     */
     927    public function test_introspect_item_no_app_password_used() {
     928        wp_set_current_user( self::$admin );
     929        $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
     930        $this->assertErrorResponse( 'rest_no_authenticated_app_password', $response, 404 );
     931    }
     932
     933    /**
     934     * @ticket 52275
     935     */
     936    public function test_introspect_item_password_invalid() {
     937        $this->setup_app_password_authenticated_request();
     938        add_action(
     939            'application_password_did_authenticate',
     940            function() {
     941                $GLOBALS['wp_rest_application_password_uuid'] = 'invalid_uuid';
     942            }
     943        );
     944
     945        $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
     946        $this->assertErrorResponse( 'rest_application_password_not_found', $response, 500 );
     947    }
     948
     949    /**
     950     * Sets up a REST API request to be authenticated using an App Password.
     951     *
     952     * @since 5.7.0
     953     *
     954     * @return array The created App Password.
     955     */
     956    private function setup_app_password_authenticated_request() {
     957        list( $password, $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'Test' ) );
     958
     959        $_SERVER['PHP_AUTH_USER'] = get_userdata( self::$admin )->user_login;
     960        $_SERVER['PHP_AUTH_PW']   = $password;
     961
     962        $GLOBALS['current_user'] = null;
     963
     964        add_filter( 'application_password_is_api_request', '__return_true' );
     965
     966        return $item;
     967    }
    880968}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r49925 r50065  
    120120            '/wp/v2/users/me',
    121121            '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords',
     122            '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/introspect',
    122123            '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)',
    123124            '/wp/v2/comments',
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r50030 r50065  
    49864986                    ],
    49874987                    "args": []
     4988                }
     4989            ]
     4990        },
     4991        "/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/introspect": {
     4992            "namespace": "wp/v2",
     4993            "methods": [
     4994                "GET"
     4995            ],
     4996            "endpoints": [
     4997                {
     4998                    "methods": [
     4999                        "GET"
     5000                    ],
     5001                    "args": {
     5002                        "context": {
     5003                            "description": "Scope under which the request is made; determines fields present in response.",
     5004                            "type": "string",
     5005                            "enum": [
     5006                                "view",
     5007                                "embed",
     5008                                "edit"
     5009                            ],
     5010                            "default": "view",
     5011                            "required": false
     5012                        }
     5013                    }
    49885014                }
    49895015            ]
Note: See TracChangeset for help on using the changeset viewer.