Make WordPress Core

Changeset 50065


Ignore:
Timestamp:
01/29/2021 12:05:20 AM (5 years 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.