<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[50065] trunk: App Passwords: Introduce introspection endpoint.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/50065">50065</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/50065","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>TimothyBlynJacobs</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2021-01-29 00:05:20 +0000 (Fri, 29 Jan 2021)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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 <a href="https://core.trac.wordpress.org/ticket/52275">#52275</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesdefaultfiltersphp">trunk/src/wp-includes/default-filters.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestapplicationpasswordscontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php</a></li>
<li><a href="#trunksrcwpincludesrestapiphp">trunk/src/wp-includes/rest-api.php</a></li>
<li><a href="#trunktestsphpunittestsauthphp">trunk/tests/phpunit/tests/auth.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestapplicationpasswordscontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestschemasetupphp">trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php</a></li>
<li><a href="#trunktestsqunitfixtureswpapigeneratedjs">trunk/tests/qunit/fixtures/wp-api-generated.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesdefaultfiltersphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-filters.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-filters.php 2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/src/wp-includes/default-filters.php   2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -280,7 +280,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' );
</span><span class="cx" style="display: block; padding: 0 10px"> add_action( 'application_password_failed_authentication', 'rest_application_password_collect_status' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-add_action( 'application_password_did_authenticate', 'rest_application_password_collect_status' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+add_action( 'application_password_did_authenticate', 'rest_application_password_collect_status', 10, 2 );
</ins><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'rest_authentication_errors', 'rest_application_password_check_errors', 90 );
</span><span class="cx" style="display: block; padding: 0 10px"> add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiendpointsclasswprestapplicationpasswordscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php       2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php 2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -59,6 +59,22 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                register_rest_route(
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->namespace,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        '/' . $this->rest_base . '/introspect',
+                       array(
+                               array(
+                                       'methods'             => WP_REST_Server::READABLE,
+                                       'callback'            => array( $this, 'get_current_item' ),
+                                       'permission_callback' => array( $this, 'get_current_item_permissions_check' ),
+                                       'args'                => array(
+                                               'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+                                       ),
+                               ),
+                               'schema' => array( $this, 'get_public_item_schema' ),
+                       )
+               );
+
+               register_rest_route(
+                       $this->namespace,
</ins><span class="cx" style="display: block; padding: 0 10px">                         '/' . $this->rest_base . '/(?P<uuid>[\w\-]+)',
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><span class="cx" style="display: block; padding: 0 10px">                                array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -374,6 +390,70 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Checks if a given request has access to get the currently used application password.
+        *
+        * @since 5.7.0
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
+        */
+       public function get_current_item_permissions_check( $request ) {
+               $user = $this->get_user( $request );
+
+               if ( is_wp_error( $user ) ) {
+                       return $user;
+               }
+
+               if ( get_current_user_id() !== $user->ID ) {
+                       return new WP_Error(
+                               'rest_cannot_introspect_app_password_for_non_authenticated_user',
+                               __( 'The authenticated Application Password can only be introspected for the current user.' ),
+                               array( 'status' => rest_authorization_required_code() )
+                       );
+               }
+
+               return true;
+       }
+
+       /**
+        * Retrieves the application password being currently used for authentication.
+        *
+        * @since 5.7.0
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+        */
+       public function get_current_item( $request ) {
+               $user = $this->get_user( $request );
+
+               if ( is_wp_error( $user ) ) {
+                       return $user;
+               }
+
+               $uuid = rest_get_authenticated_app_password();
+
+               if ( ! $uuid ) {
+                       return new WP_Error(
+                               'rest_no_authenticated_app_password',
+                               __( 'Cannot introspect Application Password.' ),
+                               array( 'status' => 404 )
+                       );
+               }
+
+               $password = WP_Application_Passwords::get_user_application_password( $user->ID, $uuid );
+
+               if ( ! $password ) {
+                       return new WP_Error(
+                               'rest_application_password_not_found',
+                               __( 'Application password not found.' ),
+                               array( 'status' => 500 )
+                       );
+               }
+
+               return $this->prepare_item_for_response( $password, $request );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Performs a permissions check for the request.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.6.0
</span></span></pre></div>
<a id="trunksrcwpincludesrestapiphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api.php        2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/src/wp-includes/rest-api.php  2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1048,18 +1048,42 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Collects the status of authenticating with an application password.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 5.6.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 5.7.0 Added the `$app_password` parameter.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @global WP_User|WP_Error|null $wp_rest_application_password_status
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @global string|null $wp_rest_application_password_uuid
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @param WP_Error $user_or_error The authenticated user or error instance.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param array    $app_password  The Application Password used to authenticate.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function rest_application_password_collect_status( $user_or_error ) {
-       global $wp_rest_application_password_status;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function rest_application_password_collect_status( $user_or_error, $app_password = array() ) {
+       global $wp_rest_application_password_status, $wp_rest_application_password_uuid;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $wp_rest_application_password_status = $user_or_error;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       if ( empty( $app_password['uuid'] ) ) {
+               $wp_rest_application_password_uuid = null;
+       } else {
+               $wp_rest_application_password_uuid = $app_password['uuid'];
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Gets the Application Password used for authenticating the request.
+ *
+ * @since 5.7.0
+ *
+ * @global string|null $wp_rest_application_password_uuid
+ *
+ * @return string|null The App Password UUID, or null if Application Passwords was not used.
+ */
+function rest_get_authenticated_app_password() {
+       global $wp_rest_application_password_uuid;
+
+       return $wp_rest_application_password_uuid;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Checks for errors when using application password-based authentication.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 5.6.0
</span></span></pre></div>
<a id="trunktestsphpunittestsauthphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/auth.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/auth.php        2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/tests/phpunit/tests/auth.php  2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -38,6 +38,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->user = clone self::$_user;
</span><span class="cx" style="display: block; padding: 0 10px">                wp_set_current_user( self::$user_id );
</span><span class="cx" style="display: block; padding: 0 10px">                update_site_option( 'using_application_passwords', 1 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        public function tearDown() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -44,7 +46,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                parent::tearDown();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Cleanup all the global state.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        function test_auth_cookie_valid() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -442,7 +444,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Create a new app-only password.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                list( $user_app_password ) = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'phpunit' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         list( $user_app_password, $item ) = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'phpunit' ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Fake a REST API request.
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'application_password_is_api_request', '__return_true' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -452,11 +454,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $_SERVER['PHP_AUTH_USER'] = 'http_auth_login';
</span><span class="cx" style="display: block; padding: 0 10px">                $_SERVER['PHP_AUTH_PW']   = 'http_auth_pass';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertSame(
-                       null,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertNull(
</ins><span class="cx" style="display: block; padding: 0 10px">                         wp_validate_application_password( null ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'Regular user account password should not be allowed for API authentication'
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertNull( rest_get_authenticated_app_password() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Not try with an App password instead.
</span><span class="cx" style="display: block; padding: 0 10px">                $_SERVER['PHP_AUTH_PW'] = $user_app_password;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -466,6 +468,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        wp_validate_application_password( null ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'Application passwords should be allowed for API authentication'
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $this->assertEquals( $item['uuid'], rest_get_authenticated_app_password() );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestapplicationpasswordscontrollerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php      2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php        2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -69,6 +69,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'wp_is_application_passwords_available', '__return_true' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        public function tearDown() {
+               parent::tearDown();
+               unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] );
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 42790
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -877,4 +882,87 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertArrayHasKey( 'last_ip', $properties );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertCount( 7, $properties );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @ticket 52275
+        */
+       public function test_introspect_item() {
+               $password = $this->setup_app_password_authenticated_request();
+               $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
+               $this->assertNotWPError( $response->as_error() );
+
+               $this->assertEquals( $password['uuid'], $response->get_data()['uuid'] );
+       }
+
+       /**
+        * @ticket 52275
+        */
+       public function test_introspect_item_specific_user() {
+               $password = $this->setup_app_password_authenticated_request();
+               $response = rest_do_request( '/wp/v2/users/' . self::$admin . '/application-passwords/introspect' );
+
+               $this->assertEquals( $password['uuid'], $response->get_data()['uuid'] );
+       }
+
+       /**
+        * @ticket 52275
+        */
+       public function test_introspect_item_logged_out() {
+               $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
+               $this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
+       }
+
+       /**
+        * @ticket 52275
+        */
+       public function test_introspect_item_wrong_user() {
+               $this->setup_app_password_authenticated_request();
+               $response = rest_do_request( '/wp/v2/users/' . self::$subscriber_id . '/application-passwords/introspect' );
+               $this->assertErrorResponse( 'rest_cannot_introspect_app_password_for_non_authenticated_user', $response, 403 );
+       }
+
+       /**
+        * @ticket 52275
+        */
+       public function test_introspect_item_no_app_password_used() {
+               wp_set_current_user( self::$admin );
+               $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
+               $this->assertErrorResponse( 'rest_no_authenticated_app_password', $response, 404 );
+       }
+
+       /**
+        * @ticket 52275
+        */
+       public function test_introspect_item_password_invalid() {
+               $this->setup_app_password_authenticated_request();
+               add_action(
+                       'application_password_did_authenticate',
+                       function() {
+                               $GLOBALS['wp_rest_application_password_uuid'] = 'invalid_uuid';
+                       }
+               );
+
+               $response = rest_do_request( '/wp/v2/users/me/application-passwords/introspect' );
+               $this->assertErrorResponse( 'rest_application_password_not_found', $response, 500 );
+       }
+
+       /**
+        * Sets up a REST API request to be authenticated using an App Password.
+        *
+        * @since 5.7.0
+        *
+        * @return array The created App Password.
+        */
+       private function setup_app_password_authenticated_request() {
+               list( $password, $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'Test' ) );
+
+               $_SERVER['PHP_AUTH_USER'] = get_userdata( self::$admin )->user_login;
+               $_SERVER['PHP_AUTH_PW']   = $password;
+
+               $GLOBALS['current_user'] = null;
+
+               add_filter( 'application_password_is_api_request', '__return_true' );
+
+               return $item;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestsrestapirestschemasetupphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php  2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php    2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -119,6 +119,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp/v2/users/(?P<id>[\\d]+)',
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp/v2/users/me',
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/introspect',
</ins><span class="cx" style="display: block; padding: 0 10px">                         '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)',
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp/v2/comments',
</span><span class="cx" style="display: block; padding: 0 10px">                        '/wp/v2/comments/(?P<id>[\\d]+)',
</span></span></pre></div>
<a id="trunktestsqunitfixtureswpapigeneratedjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/wp-api-generated.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/wp-api-generated.js    2021-01-28 19:42:39 UTC (rev 50064)
+++ trunk/tests/qunit/fixtures/wp-api-generated.js      2021-01-29 00:05:20 UTC (rev 50065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4988,6 +4988,32 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">             ]
</span><span class="cx" style="display: block; padding: 0 10px">         },
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        "/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/introspect": {
+            "namespace": "wp/v2",
+            "methods": [
+                "GET"
+            ],
+            "endpoints": [
+                {
+                    "methods": [
+                        "GET"
+                    ],
+                    "args": {
+                        "context": {
+                            "description": "Scope under which the request is made; determines fields present in response.",
+                            "type": "string",
+                            "enum": [
+                                "view",
+                                "embed",
+                                "edit"
+                            ],
+                            "default": "view",
+                            "required": false
+                        }
+                    }
+                }
+            ]
+        },
</ins><span class="cx" style="display: block; padding: 0 10px">         "/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)": {
</span><span class="cx" style="display: block; padding: 0 10px">             "namespace": "wp/v2",
</span><span class="cx" style="display: block; padding: 0 10px">             "methods": [
</span></span></pre>
</div>
</div>

</body>
</html>