Skip to content

Commit fe2053f

Browse files
App Passwords: Support an app_id to uniquely identify instances of an app.
Apps may now optionally include an `app_id` parameter when directing the user to the Authorize Application screen. This allows for instances of an application to be identified and potentially revoked or blocked. Props TimothyBlynJacobs, georgestephanis. Fixes #51583. git-svn-id: https://develop.svn.wordpress.org/trunk@49276 602fd350-edb4-49c9-b593-d223f7449a82
1 parent e4fadc6 commit fe2053f

7 files changed

Lines changed: 89 additions & 5 deletions

File tree

src/js/_enqueues/admin/auth-app.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
};
1717

1818
$approveBtn.click( function( e ) {
19-
var name = $appNameField.val();
19+
var name = $appNameField.val(),
20+
appId = $( 'input[name="app_id"]', $form ).val();
2021

2122
e.preventDefault();
2223

@@ -32,6 +33,10 @@
3233
name: name
3334
};
3435

36+
if ( appId.length > 0 ) {
37+
request.app_id = appId;
38+
}
39+
3540
/**
3641
* Filters the request data used to Authorize an Application Password request.
3742
*

src/wp-admin/authorize-application.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
$success_url = $_POST['success_url'];
1919
$reject_url = $_POST['reject_url'];
2020
$app_name = $_POST['app_name'];
21+
$app_id = $_POST['app_id'];
2122
$redirect = '';
2223

2324
if ( isset( $_POST['reject'] ) ) {
@@ -27,7 +28,13 @@
2728
$redirect = admin_url();
2829
}
2930
} elseif ( isset( $_POST['approve'] ) ) {
30-
$created = WP_Application_Passwords::create_new_application_password( get_current_user_id(), array( 'name' => $app_name ) );
31+
$created = WP_Application_Passwords::create_new_application_password(
32+
get_current_user_id(),
33+
array(
34+
'name' => $app_name,
35+
'app_id' => $app_id,
36+
)
37+
);
3138

3239
if ( is_wp_error( $created ) ) {
3340
$error = $created;
@@ -56,6 +63,7 @@
5663
$title = __( 'Authorize Application' );
5764

5865
$app_name = ! empty( $_REQUEST['app_name'] ) ? $_REQUEST['app_name'] : '';
66+
$app_id = ! empty( $_REQUEST['app_id'] ) ? $_REQUEST['app_id'] : '';
5967
$success_url = ! empty( $_REQUEST['success_url'] ) ? $_REQUEST['success_url'] : null;
6068

6169
if ( ! empty( $_REQUEST['reject_url'] ) ) {
@@ -68,7 +76,7 @@
6876

6977
$user = wp_get_current_user();
7078

71-
$request = compact( 'app_name', 'success_url', 'reject_url' );
79+
$request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' );
7280
$is_valid = wp_is_authorize_application_password_request_valid( $request, $user );
7381

7482
if ( is_wp_error( $is_valid ) ) {
@@ -183,6 +191,7 @@
183191
<form action="<?php echo esc_url( admin_url( 'authorize-application.php' ) ); ?>" method="post">
184192
<?php wp_nonce_field( 'authorize_application_password' ); ?>
185193
<input type="hidden" name="action" value="authorize_application_password" />
194+
<input type="hidden" name="app_id" value="<?php echo esc_attr( $app_id ); ?>" />
186195
<input type="hidden" name="success_url" value="<?php echo esc_url( $success_url ); ?>" />
187196
<input type="hidden" name="reject_url" value="<?php echo esc_url( $reject_url ); ?>" />
188197

src/wp-admin/includes/user.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ function admin_created_user_email( $text ) {
604604
* The array of request data. All arguments are optional and may be empty.
605605
*
606606
* @type string $app_name The suggested name of the application.
607+
* @type string $app_id A uuid provided by the application to uniquely identify it.
607608
* @type string $success_url The url the user will be redirected to after approving the application.
608609
* @type string $reject_url The url the user will be redirected to after rejecting the application.
609610
* }
@@ -635,6 +636,13 @@ function wp_is_authorize_application_password_request_valid( $request, $user ) {
635636
}
636637
}
637638

639+
if ( ! empty( $request['app_id'] ) && ! wp_is_uuid( $request['app_id'] ) ) {
640+
$error->add(
641+
'invalid_app_id',
642+
__( 'The app id must be a uuid.' )
643+
);
644+
}
645+
638646
/**
639647
* Fires before application password errors are returned.
640648
*

src/wp-includes/class-wp-application-passwords.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public static function create_new_application_password( $user_id, $args = array(
5151

5252
$new_item = array(
5353
'uuid' => wp_generate_uuid4(),
54+
'app_id' => empty( $args['app_id'] ) ? '' : $args['app_id'],
5455
'name' => $args['name'],
5556
'password' => $hashed_password,
5657
'created' => time(),

src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ protected function prepare_item_for_database( $request ) {
412412
'name' => $request['name'],
413413
);
414414

415+
if ( $request['app_id'] && ! $request['uuid'] ) {
416+
$prepared->app_id = $request['app_id'];
417+
}
418+
415419
/**
416420
* Filters an application password before it is inserted via the REST API.
417421
*
@@ -441,6 +445,7 @@ public function prepare_item_for_response( $item, $request ) {
441445

442446
$prepared = array(
443447
'uuid' => $item['uuid'],
448+
'app_id' => empty( $item['app_id'] ) ? '' : $item['app_id'],
444449
'name' => $item['name'],
445450
'created' => gmdate( 'Y-m-d\TH:i:s', $item['created'] ),
446451
'last_used' => $item['last_used'] ? gmdate( 'Y-m-d\TH:i:s', $item['last_used'] ) : null,
@@ -615,6 +620,12 @@ public function get_item_schema() {
615620
'context' => array( 'view', 'edit', 'embed' ),
616621
'readonly' => true,
617622
),
623+
'app_id' => array(
624+
'description' => __( 'A uuid provided by the application to uniquely identify it. It is recommended to use an UUID v5 with the URL or DNS namespace.' ),
625+
'type' => 'string',
626+
'format' => 'uuid',
627+
'context' => array( 'view', 'edit', 'embed' ),
628+
),
618629
'name' => array(
619630
'description' => __( 'The name of the application password.' ),
620631
'type' => 'string',

tests/phpunit/tests/rest-api/rest-application-passwords-controller.php

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,14 @@ public function test_get_item_invalid_password_uuid() {
308308
public function test_create_item() {
309309
wp_set_current_user( self::$admin );
310310

311+
$app_id = wp_generate_uuid4();
311312
$request = new WP_REST_Request( 'POST', '/wp/v2/users/me/application-passwords' );
312-
$request->set_body_params( array( 'name' => 'App' ) );
313+
$request->set_body_params(
314+
array(
315+
'name' => 'App',
316+
'app_id' => $app_id,
317+
)
318+
);
313319
$response = rest_do_request( $request );
314320

315321
$this->assertEquals( 201, $response->get_status() );
@@ -318,6 +324,7 @@ public function test_create_item() {
318324
$this->assertCount( 1, $passwords );
319325
$this->check_response( $response->get_data(), $passwords[0], true );
320326
$this->assertEquals( 'App', $response->get_data()['name'] );
327+
$this->assertEquals( $app_id, $response->get_data()['app_id'] );
321328
$this->assertNull( $response->get_data()['last_used'] );
322329
$this->assertNull( $response->get_data()['last_ip'] );
323330
}
@@ -513,6 +520,36 @@ public function test_update_item_invalid_password_uuid() {
513520
$this->assertErrorResponse( 'rest_application_password_not_found', $response, 404 );
514521
}
515522

523+
/**
524+
* @ticket 51583
525+
*/
526+
public function test_update_item_cannot_overwrite_app_id() {
527+
wp_set_current_user( self::$admin );
528+
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
529+
530+
$uuid = $item['uuid'];
531+
$request = new WP_REST_Request( 'PUT', '/wp/v2/users/me/application-passwords/' . $uuid );
532+
$request->set_body_params( array( 'app_id' => wp_generate_uuid4() ) );
533+
$response = rest_do_request( $request );
534+
$this->assertEquals( '', $response->get_data()['app_id'] );
535+
536+
$app_id = wp_generate_uuid4();
537+
538+
list( , $item ) = WP_Application_Passwords::create_new_application_password(
539+
self::$admin,
540+
array(
541+
'name' => 'App',
542+
'app_id' => $app_id,
543+
)
544+
);
545+
546+
$uuid = $item['uuid'];
547+
$request = new WP_REST_Request( 'PUT', '/wp/v2/users/me/application-passwords/' . $uuid );
548+
$request->set_body_params( array( 'app_id' => wp_generate_uuid4() ) );
549+
$response = rest_do_request( $request );
550+
$this->assertEquals( $app_id, $response->get_data()['app_id'] );
551+
}
552+
516553
/**
517554
* @ticket 42790
518555
*/
@@ -775,12 +812,14 @@ public function test_prepare_item() {
775812
*/
776813
protected function check_response( $response, $item, $password = false ) {
777814
$this->assertArrayHasKey( 'uuid', $response );
815+
$this->assertArrayHasKey( 'app_id', $response );
778816
$this->assertArrayHasKey( 'name', $response );
779817
$this->assertArrayHasKey( 'created', $response );
780818
$this->assertArrayHasKey( 'last_used', $response );
781819
$this->assertArrayHasKey( 'last_ip', $response );
782820

783821
$this->assertEquals( $item['uuid'], $response['uuid'] );
822+
$this->assertEquals( $item['app_id'], $response['app_id'] );
784823
$this->assertEquals( $item['name'], $response['name'] );
785824
$this->assertEquals( gmdate( 'Y-m-d\TH:i:s', $item['created'] ), $response['created'] );
786825

@@ -812,12 +851,13 @@ public function test_get_item_schema() {
812851
$data = $response->get_data();
813852
$properties = $data['schema']['properties'];
814853

815-
$this->assertCount( 6, $properties );
816854
$this->assertArrayHasKey( 'uuid', $properties );
855+
$this->assertArrayHasKey( 'app_id', $properties );
817856
$this->assertArrayHasKey( 'name', $properties );
818857
$this->assertArrayHasKey( 'password', $properties );
819858
$this->assertArrayHasKey( 'created', $properties );
820859
$this->assertArrayHasKey( 'last_used', $properties );
821860
$this->assertArrayHasKey( 'last_ip', $properties );
861+
$this->assertCount( 7, $properties );
822862
}
823863
}

tests/qunit/fixtures/wp-api-generated.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4917,6 +4917,11 @@ mockedApiResponse.Schema = {
49174917
"POST"
49184918
],
49194919
"args": {
4920+
"app_id": {
4921+
"description": "A machine-readable string provided by the application to uniquely identify it.",
4922+
"type": "string",
4923+
"required": false
4924+
},
49204925
"name": {
49214926
"description": "The name of the application password.",
49224927
"type": "string",
@@ -4967,6 +4972,11 @@ mockedApiResponse.Schema = {
49674972
"PATCH"
49684973
],
49694974
"args": {
4975+
"app_id": {
4976+
"description": "A machine-readable string provided by the application to uniquely identify it.",
4977+
"type": "string",
4978+
"required": false
4979+
},
49704980
"name": {
49714981
"description": "The name of the application password.",
49724982
"type": "string",

0 commit comments

Comments
 (0)